aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-08-14 00:01:18 +0200
committerdec05eba <dec05eba@protonmail.com>2020-08-14 00:01:53 +0200
commitdf3252ca6ee12bafafb5aaed298193142ff93248 (patch)
treeb51e6d6f858be3184b25f468d0c564346fc153cc
parent0d12947b1f70745d8023c89e22432f2a1353dfbb (diff)
Add /upload command to upload image/video (jpeg, png, webm and mp4)
Requires ffmpeg and ffprobe (which is part of ffmpeg) programs to be installed on the system
-rw-r--r--ui/command-processor.go1
-rw-r--r--ui/commands.go226
2 files changed, 227 insertions, 0 deletions
diff --git a/ui/command-processor.go b/ui/command-processor.go
index 514d67b..21155fa 100644
--- a/ui/command-processor.go
+++ b/ui/command-processor.go
@@ -133,6 +133,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"redact": cmdRedact,
"react": cmdReact,
"download": cmdDownload,
+ "upload": cmdUpload,
"open": cmdOpen,
"copy": cmdCopy,
"sendevent": cmdSendEvent,
diff --git a/ui/commands.go b/ui/commands.go
index 9d38396..0cbf2b0 100644
--- a/ui/commands.go
+++ b/ui/commands.go
@@ -17,11 +17,16 @@
package ui
import (
+ "bytes"
"encoding/json"
"fmt"
+ "image"
"io"
+ "io/ioutil"
"math"
"os"
+ "os/exec"
+ "path/filepath"
"regexp"
"runtime"
dbg "runtime/debug"
@@ -42,6 +47,7 @@ import (
"maunium.net/go/mautrix/id"
"maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/matrix/muksevt"
)
func cmdMe(cmd *Command) {
@@ -175,6 +181,226 @@ func cmdDownload(cmd *Command) {
cmd.Room.StartSelecting(SelectDownload, strings.Join(cmd.Args, " "))
}
+type VideoInfo struct {
+ width int
+ height int
+ duration int
+}
+
+func videoGetInfo(filePath string) (VideoInfo, error) {
+ cmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_entries", "format=duration", "--", filePath)
+ var buffer bytes.Buffer
+ cmd.Stdout = &buffer
+ if cmd.Run() != nil {
+ return VideoInfo{0, 0, 0}, errors.New("Failed to extract video dimensions using ffprobe")
+ }
+
+ var jsonData map[string]interface{}
+ if err := json.Unmarshal(buffer.Bytes(), &jsonData); err != nil {
+ return VideoInfo{0, 0, 0}, errors.New("Failed to parse ffprobe response as json")
+ }
+
+ // TODO: Check if fields do not exist or a are wrong type???
+ format := jsonData["format"].(map[string]interface{})
+ durationStr := format["duration"].(string)
+ duration, err := strconv.ParseFloat(durationStr, 64)
+ if err != nil {
+ return VideoInfo{0, 0, 0}, errors.New("Failed to parse ffprobe response as json (duration is not a string)")
+ }
+
+ streams := jsonData["streams"].([]interface{})
+ firstStream := streams[0].(map[string]interface{})
+
+ return VideoInfo{int(firstStream["width"].(float64)), int(firstStream["height"].(float64)), int(duration * 1000)}, nil
+}
+
+func videoGetThumbnail(filePath string) ([]byte, error) {
+ cmd := exec.Command("ffmpeg", "-i", filePath, "-vframes", "1", "-f", "singlejpeg", "-")
+ var buffer bytes.Buffer
+ cmd.Stdout = &buffer
+ if cmd.Run() != nil {
+ return nil, errors.New("Failed to extract thumbnail from video")
+ }
+ return buffer.Bytes(), nil
+}
+
+func uploadVideo(cmd *Command, filePath string, fileName string, fileSize int, videoFormat string) {
+ videoInfo, err := videoGetInfo(filePath)
+ if err != nil {
+ cmd.Reply("Failed to get video info, error: %v", err)
+ return
+ }
+
+ thumbnailData, err := videoGetThumbnail(filePath)
+ if err != nil {
+ cmd.Reply("Failed to extract thumbnail for video")
+ return
+ }
+
+ imageMetadata, _, err := image.DecodeConfig(bytes.NewReader(thumbnailData))
+ if err != nil {
+ cmd.Reply("Failed to decode thumbnail image")
+ return
+ }
+
+ thumbnailResp, err := cmd.Matrix.Client().UploadBytesWithName(thumbnailData, "image/jpeg", "thumbnail.jpg")
+ if err != nil {
+ cmd.Reply("Failed to upload thumbnail: %v", err)
+ return
+ }
+
+ fileData, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ cmd.Reply("Failed to read file, error: %v", err)
+ return
+ }
+
+ videoMimeType := fmt.Sprintf("video/%s", videoFormat)
+ resp, err := cmd.Matrix.Client().UploadBytesWithName(fileData, videoMimeType, fileName)
+ if err != nil {
+ cmd.Reply("Failed to upload video: %v", err)
+ return
+ }
+
+ txnID := cmd.Matrix.Client().TxnID()
+ content := event.MessageEventContent{
+ Body: fileName,
+ Info: &event.FileInfo{
+ Duration: videoInfo.duration,
+ Width: videoInfo.width,
+ Height: videoInfo.height,
+ MimeType: videoMimeType,
+ Size: fileSize,
+ ThumbnailURL: id.ContentURIString(fmt.Sprintf("mxc://%s/%s", thumbnailResp.ContentURI.Homeserver, thumbnailResp.ContentURI.FileID)),
+ ThumbnailInfo: &event.FileInfo{
+ Width: imageMetadata.Width,
+ Height: imageMetadata.Height,
+ MimeType: "image/jpeg",
+ Size: len(thumbnailData),
+ },
+ },
+ MsgType: event.MsgVideo,
+ URL: id.ContentURIString(fmt.Sprintf("mxc://%s/%s", resp.ContentURI.Homeserver, resp.ContentURI.FileID)),
+ }
+
+ _, err = cmd.Matrix.SendEvent(&muksevt.Event{
+ Event: &event.Event{
+ Content: event.Content{Parsed: &content},
+ ID: id.EventID(txnID),
+ RoomID: cmd.Room.MxRoom().ID,
+ Timestamp: time.Now().UnixNano() / 1e6,
+ Sender: cmd.Matrix.Client().UserID,
+ Type: event.EventMessage,
+ Unsigned: event.Unsigned{TransactionID: txnID},
+ },
+ })
+
+ if err != nil {
+ cmd.Reply("Failed to upload video: %v", err)
+ return
+ }
+}
+
+func uploadImage(cmd *Command, filePath string, fileName string) {
+ fileData, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ cmd.Reply("Failed to read file, error: %v", err)
+ return
+ }
+ fileSize := len(fileData)
+
+ imageMetadata, _, err := image.DecodeConfig(bytes.NewReader(fileData))
+ if err != nil {
+ cmd.Reply("Failed to decode image")
+ return
+ }
+
+ var imageType string
+ // TODO: Detect all types matrix supports
+ if fileSize >= 4 && fileData[0] == 0xFF && fileData[1] == 0xD8 && fileData[fileSize-2] == 0xFF && fileData[fileSize-1] == 0xD9 {
+ imageType = "jpeg"
+ } else if fileSize >= 8 && string(fileData[0:8]) == "\211PNG\r\n\032\n" {
+ imageType = "png"
+ }
+
+ if imageType == "" {
+ cmd.Reply("Only jpeg, png, webm and mp4 files are supported")
+ return
+ }
+
+ imageMimeType := fmt.Sprintf("image/%s", imageType)
+
+ resp, err := cmd.Matrix.Client().UploadBytesWithName(fileData, imageMimeType, fileName)
+ if err != nil {
+ cmd.Reply("Failed to upload image: %v", err)
+ return
+ }
+
+ txnID := cmd.Matrix.Client().TxnID()
+ //cmd.Reply("Uploaded image: %v", fmt.Sprintf("mxc://%s/%s", resp.ContentURI.Homeserver, resp.ContentURI.FileID))
+ content := event.MessageEventContent{
+ Body: fileName,
+ Info: &event.FileInfo{
+ Width: imageMetadata.Width,
+ Height: imageMetadata.Height,
+ MimeType: imageMimeType,
+ Size: int(fileSize),
+ // TODO: "xyz.amorgan.blurhash: " https://github.com/buckket/go-blurhash
+ },
+ MsgType: event.MsgImage,
+ URL: id.ContentURIString(fmt.Sprintf("mxc://%s/%s", resp.ContentURI.Homeserver, resp.ContentURI.FileID)),
+ }
+
+ _, err = cmd.Matrix.SendEvent(&muksevt.Event{
+ Event: &event.Event{
+ Content: event.Content{Parsed: &content},
+ ID: id.EventID(txnID),
+ RoomID: cmd.Room.MxRoom().ID,
+ Timestamp: time.Now().UnixNano() / 1e6,
+ Sender: cmd.Matrix.Client().UserID,
+ Type: event.EventMessage,
+ Unsigned: event.Unsigned{TransactionID: txnID},
+ },
+ })
+
+ if err != nil {
+ cmd.Reply("Failed to upload image: %v", err)
+ return
+ }
+}
+
+func cmdUpload(cmd *Command) {
+ filePath := cmd.RawArgs
+ fileName := filepath.Base(filePath)
+ fileExt := filepath.Ext(fileName)
+
+ // TODO: This is not safe. Instead open the file and get file size blabla?
+ stat, err := os.Stat(filePath)
+ if err != nil {
+ cmd.Reply("Failed to get file stats for %s, error: %v", filePath, err)
+ return
+ }
+ fileSize := stat.Size()
+
+ if fileSize >= 40*1024*1024 { // 40mb
+ cmd.Reply("Can't upload a file larger than 40mb. The file you are trying to upload is %fmb", float64(fileSize)/1024.0/1024.0)
+ return
+ }
+
+ var videoFormat string
+ if fileExt == ".webm" {
+ videoFormat = "webm"
+ } else if fileExt == ".mp4" {
+ videoFormat = "mp4"
+ }
+
+ if videoFormat != "" {
+ go uploadVideo(cmd, filePath, fileName, int(fileSize), videoFormat)
+ } else {
+ go uploadImage(cmd, filePath, fileName)
+ }
+}
+
func cmdOpen(cmd *Command) {
cmd.Room.StartSelecting(SelectOpen, strings.Join(cmd.Args, " "))
}