From 63adb9bd79165cd13bda6818e75b21b04ba5d555 Mon Sep 17 00:00:00 2001
From: Norbert Kwizera <norkans7@gmail.com>
Date: Tue, 5 Sep 2023 13:46:28 +0200
Subject: [PATCH] Add support to filter media by dimensions and content type
 precedence for media support config

---
 handlers/line/line.go  |  9 +++++----
 handlers/media.go      | 20 +++++++++++++++++---
 handlers/media_test.go | 22 ++++++++++++++++++++++
 3 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/handlers/line/line.go b/handlers/line/line.go
index fe73f68c5..adf9c4f48 100644
--- a/handlers/line/line.go
+++ b/handlers/line/line.go
@@ -32,10 +32,11 @@ var (
 
 // see https://developers.line.biz/en/reference/messaging-api/#message-objects
 var mediaSupport = map[handlers.MediaType]handlers.MediaTypeSupport{
-	handlers.MediaTypeImage:       {Types: []string{"image/jpeg", "image/png"}, MaxBytes: 10 * 1024 * 1024},
-	handlers.MediaTypeAudio:       {Types: []string{"audio/mp4"}, MaxBytes: 200 * 1024 * 1024},
-	handlers.MediaTypeVideo:       {Types: []string{"video/mp4"}, MaxBytes: 200 * 1024 * 1024},
-	handlers.MediaTypeApplication: {},
+	handlers.MediaType("image/webp"): {Types: []string{"image/webp"}, MaxBytes: 100 * 1024, MaxWidth: 512, MaxHeight: 512},
+	handlers.MediaTypeImage:          {Types: []string{"image/jpeg", "image/png"}, MaxBytes: 10 * 1024 * 1024},
+	handlers.MediaTypeAudio:          {Types: []string{"audio/mp4"}, MaxBytes: 200 * 1024 * 1024},
+	handlers.MediaTypeVideo:          {Types: []string{"video/mp4"}, MaxBytes: 200 * 1024 * 1024},
+	handlers.MediaTypeApplication:    {},
 }
 
 func init() {
diff --git a/handlers/media.go b/handlers/media.go
index 2340ea0d4..6cebfffa0 100644
--- a/handlers/media.go
+++ b/handlers/media.go
@@ -20,8 +20,10 @@ const (
 )
 
 type MediaTypeSupport struct {
-	Types    []string
-	MaxBytes int
+	Types     []string
+	MaxBytes  int
+	MaxWidth  int
+	MaxHeight int
 }
 
 // Attachment is a resolved attachment
@@ -82,7 +84,10 @@ func resolveAttachment(ctx context.Context, b courier.Backend, attachment string
 	}
 
 	mediaType, _ := parseContentType(media.ContentType())
-	mediaSupport := support[mediaType]
+	mediaSupport, ok := support[MediaType(media.ContentType())]
+	if !ok {
+		mediaSupport = support[mediaType]
+	}
 
 	// our candidates are the uploaded media and any alternates of the same media type
 	candidates := append([]courier.Media{media}, filterMediaByType(media.Alternates(), mediaType)...)
@@ -97,6 +102,11 @@ func resolveAttachment(ctx context.Context, b courier.Backend, attachment string
 		candidates = filterMediaBySize(candidates, mediaSupport.MaxBytes)
 	}
 
+	// narrow down the candidates to the ones that don't exceed our max dimensions
+	if mediaSupport.MaxWidth > 0 && mediaSupport.MaxHeight > 0 {
+		candidates = filterMediaByDimensions(candidates, mediaSupport.MaxWidth, mediaSupport.MaxHeight)
+	}
+
 	// if we have no candidates, we can't use this media
 	if len(candidates) == 0 {
 		return nil, nil
@@ -142,6 +152,10 @@ func filterMediaBySize(in []courier.Media, maxBytes int) []courier.Media {
 	return filterMedia(in, func(m courier.Media) bool { return m.Size() <= maxBytes })
 }
 
+func filterMediaByDimensions(in []courier.Media, maxWidth int, MaxHeight int) []courier.Media {
+	return filterMedia(in, func(m courier.Media) bool { return m.Width() <= maxWidth && m.Height() <= MaxHeight })
+}
+
 func filterMedia(in []courier.Media, f func(courier.Media) bool) []courier.Media {
 	filtered := make([]courier.Media, 0, len(in))
 	for _, m := range in {
diff --git a/handlers/media_test.go b/handlers/media_test.go
index 56dfd67ad..33e7707fd 100644
--- a/handlers/media_test.go
+++ b/handlers/media_test.go
@@ -134,6 +134,28 @@ func TestResolveAttachments(t *testing.T) {
 			mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{},
 			err:          "invalid attachment format: http://mock.com/1234/test.jpg",
 		},
+		{ // 14: resolveable uploaded image URL with matching dimensions
+			attachments:  []string{"image/jpeg:http://mock.com/1234/test.jpg"},
+			mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 1000, MaxHeight: 1000}},
+			allowURLOnly: true,
+			resolved: []*handlers.Attachment{
+				{Type: handlers.MediaTypeImage, Name: "test.jpg", ContentType: "image/jpeg", URL: "http://mock.com/1234/test.jpg", Media: imageJPG, Thumbnail: nil},
+			},
+		},
+		{ // 15: resolveable uploaded image URL without matching dimensions
+			attachments:  []string{"image/jpeg:http://mock.com/1234/test.jpg"},
+			mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 100, MaxHeight: 100}},
+			allowURLOnly: true,
+			resolved:     []*handlers.Attachment{},
+		},
+		{ // 16: resolveable uploaded image URL without matching dimensions by specific content type precendence
+			attachments:  []string{"image/jpeg:http://mock.com/1234/test.jpg"},
+			mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 100, MaxHeight: 100}, handlers.MediaType("image/jpeg"): {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 1000, MaxHeight: 1000}},
+			allowURLOnly: true,
+			resolved: []*handlers.Attachment{
+				{Type: handlers.MediaTypeImage, Name: "test.jpg", ContentType: "image/jpeg", URL: "http://mock.com/1234/test.jpg", Media: imageJPG, Thumbnail: nil},
+			},
+		},
 	}
 
 	for i, tc := range tcs {