From 4e2c73efe3f0d5f557d1d2a403a0d6349e756344 Mon Sep 17 00:00:00 2001 From: Karmanyaah Malhotra Date: Sun, 7 Mar 2021 11:46:25 -0500 Subject: [PATCH] Groupme -> Matrix Images --- ROADMAP.md | 3 + groupmeExt/message.go | 25 +++ portal.go | 388 +++++++++++++++++++++++++----------------- puppet.go | 20 +-- 4 files changed, 264 insertions(+), 172 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f771061..537b62b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,6 +25,9 @@ * [x] Plain text * [ ] Formatted messages * [ ] Media/files + * [x] Images + * [ ] Videos + * [ ] Other Files * [ ] Location messages * [ ] Contact messages * [ ] Replies diff --git a/groupmeExt/message.go b/groupmeExt/message.go index 18a9c58..02a2b0e 100644 --- a/groupmeExt/message.go +++ b/groupmeExt/message.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" + "net/http" "github.com/karmanyaahm/groupme" ) @@ -31,3 +33,26 @@ func (m *Message) Value() (driver.Value, error) { } return e, nil } + +//DownloadImage helper function to download image from groupme; +// append .large/.preview/.avatar to get various sizes +func DownloadImage(URL string) (bytes *[]byte, mime string, err error) { + //TODO check its actually groupme? + response, err := http.Get(URL) + if err != nil { + return nil, "", errors.New("Failed to download avatar: " + err.Error()) + } + defer response.Body.Close() + + image, err := ioutil.ReadAll(response.Body) + bytes = &image + if err != nil { + return nil, "", errors.New("Failed to read downloaded image:" + err.Error()) + } + + mime = response.Header.Get("Content-Type") + if len(mime) == 0 { + mime = http.DetectContentType(image) + } + return +} diff --git a/portal.go b/portal.go index 26c175e..bdeb84d 100644 --- a/portal.go +++ b/portal.go @@ -30,6 +30,7 @@ import ( "io/ioutil" "math/rand" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -1258,6 +1259,219 @@ func (portal *Portal) sendMessageDirect(intent *appservice.IntentAPI, eventType } } +func (portal *Portal) handleAttachment(intent *appservice.IntentAPI, attachment *groupme.Attachment, ts int64) error { + switch attachment.Type { + case "image": + imgData, mime, err := groupmeExt.DownloadImage(attachment.URL) + if err != nil { + return fmt.Errorf("failed to load media info: %w", err) + } + + var width, height int + if strings.HasPrefix(mime, "image/") { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(*imgData)) + width, height = cfg.Width, cfg.Height + } + data, uploadMimeType, file := portal.encryptFile(*imgData, mime) + + uploaded, err := portal.uploadWithRetry(intent, data, uploadMimeType, MediaUploadRetries) + if err != nil { + if errors.Is(err, mautrix.MTooLarge) { + return errors.New("homeserver rejected too large file") + } else if httpErr := err.(mautrix.HTTPError); httpErr.IsStatus(413) { + return errors.New("proxy rejected too large file") + } else { + return fmt.Errorf("failed to upload media: %w", err) + } + } + attachmentUrl, _ := url.Parse(attachment.URL) + urlParts := strings.Split(attachmentUrl.Path, ".") + var fname1, fname2 string + if len(urlParts) == 2 { + fname1, fname2 = urlParts[1], urlParts[0] + } else if len(urlParts) > 2 { + fname1, fname2 = urlParts[2], urlParts[1] + } //TODO abstract groupme url parsing in groupmeExt + fname := fmt.Sprintf("%s.%s", fname1, fname2) + + content := &event.MessageEventContent{ + Body: fname, + File: file, + Info: &event.FileInfo{ + Size: len(data), + MimeType: mime, + Width: width, + Height: height, + //Duration: int(msg.length), + }, + } + if content.File != nil { + content.File.URL = uploaded.ContentURI.CUString() + } else { + content.URL = uploaded.ContentURI.CUString() + } + //TODO thumbnail since groupme supports it anyway + content.MsgType = event.MsgImage + _, _ = intent.UserTyping(portal.MXID, false, 0) + + eventType := event.EventMessage + + _, err = portal.sendMessage(intent, eventType, content, ts) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", "TODOID", err) + return nil + } + + default: + portal.log.Warnln("Unable to handle groupme attachment type", attachment.Type) + return fmt.Errorf("Unable to handle groupme attachment type %s", attachment.Type) + } + return nil +} +func (portal *Portal) HandleMediaMessage(source *User, msg mediaMessage) { + // intent := portal.startHandling(source, msg.info) + // if intent == nil { + // return + // } + // + // data, err := msg.download() + // if err == whatsapp.ErrMediaDownloadFailedWith404 || err == whatsapp.ErrMediaDownloadFailedWith410 { + // portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", msg.info.Id, err) + // _, err = source.Conn.LoadMediaInfo(msg.info.RemoteJid, msg.info.Id, msg.info.FromMe) + // if err != nil { + // portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to load media info: %w", err)) + // return + // } + // data, err = msg.download() + // } + // if err == whatsapp.ErrNoURLPresent { + // portal.log.Debugfln("No URL present error for media message %s, ignoring...", msg.info.Id) + // return + // } else if err != nil { + // portal.sendMediaBridgeFailure(source, intent, msg.info, err) + // return + // } + // + // var width, height int + // if strings.HasPrefix(msg.mimeType, "image/") { + // cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) + // width, height = cfg.Width, cfg.Height + // } + // + // data, uploadMimeType, file := portal.encryptFile(data, msg.mimeType) + // + // uploaded, err := portal.uploadWithRetry(intent, data, uploadMimeType, MediaUploadRetries) + // if err != nil { + // if errors.Is(err, mautrix.MTooLarge) { + // portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("homeserver rejected too large file")) + // } else if httpErr := err.(mautrix.HTTPError); httpErr.IsStatus(413) { + // portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("proxy rejected too large file")) + // } else { + // portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to upload media: %w", err)) + // } + // return + // } + // + // if msg.fileName == "" { + // mimeClass := strings.Split(msg.mimeType, "/")[0] + // switch mimeClass { + // case "application": + // msg.fileName = "file" + // default: + // msg.fileName = mimeClass + // } + // + // exts, _ := mime.ExtensionsByType(msg.mimeType) + // if exts != nil && len(exts) > 0 { + // msg.fileName += exts[0] + // } + // } + // + // content := &event.MessageEventContent{ + // Body: msg.fileName, + // File: file, + // Info: &event.FileInfo{ + // Size: len(data), + // MimeType: msg.mimeType, + // Width: width, + // Height: height, + // Duration: int(msg.length), + // }, + // } + // if content.File != nil { + // content.File.URL = uploaded.ContentURI.CUString() + // } else { + // content.URL = uploaded.ContentURI.CUString() + // } + // portal.SetReply(content, msg.context) + // + // if msg.thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail { + // thumbnailMime := http.DetectContentType(msg.thumbnail) + // thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.thumbnail)) + // thumbnailSize := len(msg.thumbnail) + // thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(msg.thumbnail, thumbnailMime) + // uploadedThumbnail, err := intent.UploadBytes(thumbnail, thumbnailUploadMime) + // if err != nil { + // portal.log.Warnfln("Failed to upload thumbnail for %s: %v", msg.info.Id, err) + // } else if uploadedThumbnail != nil { + // if thumbnailFile != nil { + // thumbnailFile.URL = uploadedThumbnail.ContentURI.CUString() + // content.Info.ThumbnailFile = thumbnailFile + // } else { + // content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() + // } + // content.Info.ThumbnailInfo = &event.FileInfo{ + // Size: thumbnailSize, + // Width: thumbnailCfg.Width, + // Height: thumbnailCfg.Height, + // MimeType: thumbnailMime, + // } + // } + // } + // + // switch strings.ToLower(strings.Split(msg.mimeType, "/")[0]) { + // case "image": + // if !msg.sendAsSticker { + // content.MsgType = event.MsgImage + // } + // case "video": + // content.MsgType = event.MsgVideo + // case "audio": + // content.MsgType = event.MsgAudio + // default: + // content.MsgType = event.MsgFile + // } + // + // _, _ = intent.UserTyping(portal.MXID, false, 0) + // ts := int64(msg.info.Timestamp * 1000) + // eventType := event.EventMessage + // if msg.sendAsSticker { + // eventType = event.EventSticker + // } + // resp, err := portal.sendMessage(intent, eventType, content, ts) + // if err != nil { + // portal.log.Errorfln("Failed to handle message %s: %v", msg.info.Id, err) + // return + // } + // + // if len(msg.caption) > 0 { + // captionContent := &event.MessageEventContent{ + // Body: msg.caption, + // MsgType: event.MsgNotice, + // } + // + // portal.bridge.Formatter.ParseWhatsApp(captionContent, msg.context.MentionedJID) + // + // _, err := portal.sendMessage(intent, event.EventMessage, captionContent, ts) + // if err != nil { + // portal.log.Warnfln("Failed to handle caption of message %s: %v", msg.info.Id, err) + // } + // // TODO store caption mxid? + // } + // + // portal.finishHandling(source, msg.info.Source, resp.EventID) +} + func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message) { intent := portal.startHandling(source, message) if intent == nil { @@ -1268,6 +1482,12 @@ func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message) Body: message.Text, MsgType: event.MsgText, } + for _, a := range message.Attachments { + err := portal.handleAttachment(intent, a, message.CreatedAt.ToTime().Unix()) + if err != nil { + portal.sendMediaBridgeFailure(source, intent, *message, err) + } + } // portal.bridge.Formatter.ParseWhatsApp(content, message.ContextInfo.MentionedJID) // portal.SetReply(content, message.ContextInfo) @@ -1384,18 +1604,18 @@ func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message) // portal.finishHandling(source, message.Info.Source, resp.EventID) //} -//func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, bridgeErr error) { -// portal.log.Errorfln("Failed to bridge media for %s: %v", info.Id, bridgeErr) -// resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ -// MsgType: event.MsgNotice, -// Body: "Failed to bridge media", -// }, int64(info.Timestamp*1000)) -// if err != nil { -// portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) -// } else { -// portal.finishHandling(source, info.Source, resp.EventID) -// } -//} +func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, message groupme.Message, bridgeErr error) { + portal.log.Errorfln("Failed to bridge media for %s: %v", message.UserID.String(), bridgeErr) + resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Failed to bridge media", + }, int64(message.CreatedAt.ToTime().Unix()*1000)) + if err != nil { + portal.log.Errorfln("Failed to send media download error message for %s: %v", message.UserID.String(), err) + } else { + portal.finishHandling(source, &message, resp.EventID) + } +} func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) { if !portal.Encrypted { @@ -1504,150 +1724,6 @@ func (portal *Portal) uploadWithRetry(intent *appservice.IntentAPI, data []byte, } } -func (portal *Portal) HandleMediaMessage(source *User, msg mediaMessage) { - // intent := portal.startHandling(source, msg.info) - // if intent == nil { - // return - // } - - // data, err := msg.download() - // if err == whatsapp.ErrMediaDownloadFailedWith404 || err == whatsapp.ErrMediaDownloadFailedWith410 { - // portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", msg.info.Id, err) - // _, err = source.Conn.LoadMediaInfo(msg.info.RemoteJid, msg.info.Id, msg.info.FromMe) - // if err != nil { - // portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to load media info: %w", err)) - // return - // } - // data, err = msg.download() - // } - // if err == whatsapp.ErrNoURLPresent { - // portal.log.Debugfln("No URL present error for media message %s, ignoring...", msg.info.Id) - // return - // } else if err != nil { - // portal.sendMediaBridgeFailure(source, intent, msg.info, err) - // return - // } - - // var width, height int - // if strings.HasPrefix(msg.mimeType, "image/") { - // cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) - // width, height = cfg.Width, cfg.Height - // } - - // data, uploadMimeType, file := portal.encryptFile(data, msg.mimeType) - - // uploaded, err := portal.uploadWithRetry(intent, data, uploadMimeType, MediaUploadRetries) - // if err != nil { - // if errors.Is(err, mautrix.MTooLarge) { - // portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("homeserver rejected too large file")) - // } else if httpErr := err.(mautrix.HTTPError); httpErr.IsStatus(413) { - // portal.sendMediaBridgeFailure(source, intent, msg.info, errors.New("proxy rejected too large file")) - // } else { - // portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to upload media: %w", err)) - // } - // return - // } - - // if msg.fileName == "" { - // mimeClass := strings.Split(msg.mimeType, "/")[0] - // switch mimeClass { - // case "application": - // msg.fileName = "file" - // default: - // msg.fileName = mimeClass - // } - - // exts, _ := mime.ExtensionsByType(msg.mimeType) - // if exts != nil && len(exts) > 0 { - // msg.fileName += exts[0] - // } - // } - - // content := &event.MessageEventContent{ - // Body: msg.fileName, - // File: file, - // Info: &event.FileInfo{ - // Size: len(data), - // MimeType: msg.mimeType, - // Width: width, - // Height: height, - // Duration: int(msg.length), - // }, - // } - // if content.File != nil { - // content.File.URL = uploaded.ContentURI.CUString() - // } else { - // content.URL = uploaded.ContentURI.CUString() - // } - // portal.SetReply(content, msg.context) - - // if msg.thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail { - // thumbnailMime := http.DetectContentType(msg.thumbnail) - // thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.thumbnail)) - // thumbnailSize := len(msg.thumbnail) - // thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(msg.thumbnail, thumbnailMime) - // uploadedThumbnail, err := intent.UploadBytes(thumbnail, thumbnailUploadMime) - // if err != nil { - // portal.log.Warnfln("Failed to upload thumbnail for %s: %v", msg.info.Id, err) - // } else if uploadedThumbnail != nil { - // if thumbnailFile != nil { - // thumbnailFile.URL = uploadedThumbnail.ContentURI.CUString() - // content.Info.ThumbnailFile = thumbnailFile - // } else { - // content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() - // } - // content.Info.ThumbnailInfo = &event.FileInfo{ - // Size: thumbnailSize, - // Width: thumbnailCfg.Width, - // Height: thumbnailCfg.Height, - // MimeType: thumbnailMime, - // } - // } - // } - - // switch strings.ToLower(strings.Split(msg.mimeType, "/")[0]) { - // case "image": - // if !msg.sendAsSticker { - // content.MsgType = event.MsgImage - // } - // case "video": - // content.MsgType = event.MsgVideo - // case "audio": - // content.MsgType = event.MsgAudio - // default: - // content.MsgType = event.MsgFile - // } - - // _, _ = intent.UserTyping(portal.MXID, false, 0) - // ts := int64(msg.info.Timestamp * 1000) - // eventType := event.EventMessage - // if msg.sendAsSticker { - // eventType = event.EventSticker - // } - // resp, err := portal.sendMessage(intent, eventType, content, ts) - // if err != nil { - // portal.log.Errorfln("Failed to handle message %s: %v", msg.info.Id, err) - // return - // } - - // if len(msg.caption) > 0 { - // captionContent := &event.MessageEventContent{ - // Body: msg.caption, - // MsgType: event.MsgNotice, - // } - - // portal.bridge.Formatter.ParseWhatsApp(captionContent, msg.context.MentionedJID) - - // _, err := portal.sendMessage(intent, event.EventMessage, captionContent, ts) - // if err != nil { - // portal.log.Warnfln("Failed to handle caption of message %s: %v", msg.info.Id, err) - // } - // // TODO store caption mxid? - // } - - // portal.finishHandling(source, msg.info.Source, resp.EventID) -} - func makeMessageID() *string { b := make([]byte, 10) rand.Read(b) diff --git a/puppet.go b/puppet.go index ac5c165..6e3cc75 100644 --- a/puppet.go +++ b/puppet.go @@ -18,8 +18,6 @@ package main import ( "fmt" - "io/ioutil" - "net/http" "regexp" "strings" @@ -30,6 +28,7 @@ import ( "maunium.net/go/mautrix/id" "github.com/karmanyaahm/matrix-groupme-go/database" + "github.com/karmanyaahm/matrix-groupme-go/groupmeExt" "github.com/karmanyaahm/matrix-groupme-go/types" whatsappExt "github.com/karmanyaahm/matrix-groupme-go/whatsapp-ext" ) @@ -199,24 +198,13 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar string) bool { } //TODO check its actually groupme? - response, err := http.Get(avatar + ".large") + image, mime, err := groupmeExt.DownloadImage(avatar + ".large") if err != nil { - puppet.log.Warnln("Failed to download avatar:", err) - return false - } - defer response.Body.Close() - - image, err := ioutil.ReadAll(response.Body) - if err != nil { - puppet.log.Warnln("Failed to read downloaded avatar:", err) + puppet.log.Warnln(err) return false } - mime := response.Header.Get("Content-Type") - if len(mime) == 0 { - mime = http.DetectContentType(image) - } - resp, err := puppet.DefaultIntent().UploadBytes(image, mime) + resp, err := puppet.DefaultIntent().UploadBytes(*image, mime) if err != nil { puppet.log.Warnln("Failed to upload avatar:", err) return false