The picker accepts one MP4 or WebM, the thread lightbox zooms photos, and Discord gets the video as a file on a follow-up message so photo embeds still render.
This commit is contained in:
+62
-27
@@ -22,6 +22,7 @@ import (
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"plumber/internal/blob"
|
||||
"plumber/internal/events"
|
||||
"plumber/internal/store"
|
||||
)
|
||||
|
||||
@@ -109,18 +110,18 @@ func (s *Server) postImagesFromForm(
|
||||
r *http.Request,
|
||||
postID string,
|
||||
existing []store.PostImage,
|
||||
) ([]store.PostImage, []string, error) {
|
||||
) ([]store.PostImage, []string, []events.Media, error) {
|
||||
if r.MultipartForm == nil {
|
||||
return append([]store.PostImage(nil), existing...), nil, nil
|
||||
return append([]store.PostImage(nil), existing...), nil, nil, nil
|
||||
}
|
||||
retained, err := retainedPostImages(r.MultipartForm, existing)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
files := r.MultipartForm.File["images"]
|
||||
descriptions := r.MultipartForm.Value["image_description"]
|
||||
if len(descriptions) > len(files) {
|
||||
return nil, nil, invalidPostImage("Image descriptions do not match selected images.", nil)
|
||||
return nil, nil, nil, invalidPostImage("Image descriptions do not match selected images.", nil)
|
||||
}
|
||||
kinds := make([]string, len(files))
|
||||
newImages, newVideos := 0, 0
|
||||
@@ -134,13 +135,13 @@ func (s *Server) postImagesFromForm(
|
||||
}
|
||||
retainedImages, retainedVideos := countPostMedia(retained)
|
||||
if retainedImages+newImages > store.MaxPostImages {
|
||||
return nil, nil, invalidPostImage("You can attach up to 4 images.", nil)
|
||||
return nil, nil, nil, invalidPostImage("You can attach up to 4 images.", nil)
|
||||
}
|
||||
if retainedVideos+newVideos > store.MaxPostVideos {
|
||||
return nil, nil, invalidPostImage("You can attach one video.", nil)
|
||||
return nil, nil, nil, invalidPostImage("You can attach one video.", nil)
|
||||
}
|
||||
if len(files) > 0 && !s.cfg.Blob.Enabled() {
|
||||
return nil, nil, &postImageRequestError{
|
||||
return nil, nil, nil, &postImageRequestError{
|
||||
status: http.StatusServiceUnavailable,
|
||||
message: "Image uploads are not configured on this server.",
|
||||
}
|
||||
@@ -148,24 +149,28 @@ func (s *Server) postImagesFromForm(
|
||||
|
||||
images := append([]store.PostImage(nil), retained...)
|
||||
newKeys := make([]string, 0, len(files))
|
||||
held := make([]events.Media, 0, len(files))
|
||||
for i, header := range files {
|
||||
description := ""
|
||||
if i < len(descriptions) {
|
||||
description = strings.TrimSpace(descriptions[i])
|
||||
}
|
||||
if len([]rune(description)) > store.MaxImageDescriptionRunes {
|
||||
closeHeldMedia(held)
|
||||
s.deletePostImageObjects(newKeys)
|
||||
return nil, nil, invalidPostImage("Image descriptions must be 500 characters or fewer.", nil)
|
||||
return nil, nil, nil, invalidPostImage("Image descriptions must be 500 characters or fewer.", nil)
|
||||
}
|
||||
item, objectKey, err := s.uploadPostMedia(ctx, postID, header, kinds[i], description)
|
||||
item, objectKey, media, err := s.uploadPostMedia(ctx, postID, header, kinds[i], description)
|
||||
if err != nil {
|
||||
closeHeldMedia(held)
|
||||
s.deletePostImageObjects(newKeys)
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
newKeys = append(newKeys, objectKey)
|
||||
images = append(images, item)
|
||||
held = append(held, media)
|
||||
}
|
||||
return images, newKeys, nil
|
||||
return images, newKeys, held, nil
|
||||
}
|
||||
|
||||
func countPostMedia(items []store.PostImage) (images, videos int) {
|
||||
@@ -179,18 +184,26 @@ func countPostMedia(items []store.PostImage) (images, videos int) {
|
||||
return images, videos
|
||||
}
|
||||
|
||||
func closeHeldMedia(media []events.Media) {
|
||||
for i := range media {
|
||||
if media[i].Body != nil {
|
||||
_ = media[i].Body.Close()
|
||||
media[i].Body = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) uploadPostMedia(
|
||||
ctx context.Context,
|
||||
postID string,
|
||||
header *multipart.FileHeader,
|
||||
kind, description string,
|
||||
) (store.PostImage, string, error) {
|
||||
) (store.PostImage, string, events.Media, error) {
|
||||
if kind == store.MediaKindVideo {
|
||||
prepared, err := preparePostVideo(header)
|
||||
if err != nil {
|
||||
return store.PostImage{}, "", err
|
||||
return store.PostImage{}, "", events.Media{}, err
|
||||
}
|
||||
defer prepared.body.Close()
|
||||
mediaID := uuid.NewString()
|
||||
objectKey := path.Join("post-videos", postID, mediaID+prepared.extension)
|
||||
publicURL, err := s.cfg.Blob.Upload(ctx, blob.FileUpload{
|
||||
@@ -200,12 +213,28 @@ func (s *Server) uploadPostMedia(
|
||||
Size: prepared.size,
|
||||
})
|
||||
if err != nil {
|
||||
return store.PostImage{}, "", &postImageRequestError{
|
||||
_ = prepared.body.Close()
|
||||
return store.PostImage{}, "", events.Media{}, &postImageRequestError{
|
||||
status: http.StatusServiceUnavailable,
|
||||
message: "Could not upload video. Try again later.",
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
media := events.Media{
|
||||
Name: mediaID + prepared.extension,
|
||||
ContentType: prepared.contentType,
|
||||
Kind: store.MediaKindVideo,
|
||||
Size: prepared.size,
|
||||
}
|
||||
if seeker, ok := prepared.body.(io.Seeker); ok {
|
||||
if _, err := seeker.Seek(0, io.SeekStart); err == nil {
|
||||
media.Body = prepared.body
|
||||
} else {
|
||||
_ = prepared.body.Close()
|
||||
}
|
||||
} else {
|
||||
_ = prepared.body.Close()
|
||||
}
|
||||
return store.PostImage{
|
||||
ID: mediaID,
|
||||
PostID: postID,
|
||||
@@ -213,11 +242,11 @@ func (s *Server) uploadPostMedia(
|
||||
PublicURL: publicURL,
|
||||
Description: description,
|
||||
Kind: store.MediaKindVideo,
|
||||
}, objectKey, nil
|
||||
}, objectKey, media, nil
|
||||
}
|
||||
prepared, err := preparePostImage(header)
|
||||
if err != nil {
|
||||
return store.PostImage{}, "", err
|
||||
return store.PostImage{}, "", events.Media{}, err
|
||||
}
|
||||
imageID := uuid.NewString()
|
||||
objectKey := path.Join("post-images", postID, imageID+prepared.extension)
|
||||
@@ -228,22 +257,28 @@ func (s *Server) uploadPostMedia(
|
||||
Size: int64(len(prepared.body)),
|
||||
})
|
||||
if err != nil {
|
||||
return store.PostImage{}, "", &postImageRequestError{
|
||||
return store.PostImage{}, "", events.Media{}, &postImageRequestError{
|
||||
status: http.StatusServiceUnavailable,
|
||||
message: "Could not upload image. Try again later.",
|
||||
cause: err,
|
||||
}
|
||||
}
|
||||
return store.PostImage{
|
||||
ID: imageID,
|
||||
PostID: postID,
|
||||
ObjectKey: objectKey,
|
||||
PublicURL: publicURL,
|
||||
Description: description,
|
||||
Kind: store.MediaKindImage,
|
||||
Width: prepared.width,
|
||||
Height: prepared.height,
|
||||
}, objectKey, nil
|
||||
ID: imageID,
|
||||
PostID: postID,
|
||||
ObjectKey: objectKey,
|
||||
PublicURL: publicURL,
|
||||
Description: description,
|
||||
Kind: store.MediaKindImage,
|
||||
Width: prepared.width,
|
||||
Height: prepared.height,
|
||||
}, objectKey, events.Media{
|
||||
Name: imageID + prepared.extension,
|
||||
ContentType: prepared.contentType,
|
||||
Kind: store.MediaKindImage,
|
||||
Size: int64(len(prepared.body)),
|
||||
Bytes: prepared.body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func retainedPostImages(form *multipart.Form, existing []store.PostImage) ([]store.PostImage, error) {
|
||||
|
||||
Reference in New Issue
Block a user