From 70ab2bba0dd04e688d7a2e7417954f4eebd645e0 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Sat, 29 Jul 2023 13:42:26 -0700 Subject: [PATCH 1/8] feat: add contentType support to Attachment --- .../modmailviewer/data/Attachment.java | 21 ++++++++++++------- .../khakers/modmailviewer/data/User.java | 4 ++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java b/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java index 5297193d..4b9388d7 100644 --- a/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java +++ b/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java @@ -3,14 +3,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.bson.BsonType; import org.bson.codecs.pojo.annotations.BsonRepresentation; +import org.jetbrains.annotations.Nullable; public record Attachment( - @BsonRepresentation(BsonType.STRING) - long id, - String filename, - String url, - @JsonProperty("is_image") - boolean isImage, - int size + @BsonRepresentation(BsonType.STRING) + long id, + String filename, + String url, + //isImage does not correctly Identify whether this is an image + @JsonProperty("is_image") + boolean isImage, + int size, + + //Requires modmail enhanced feature support + @JsonProperty("content_type") + @Nullable + String contentType ) { } diff --git a/src/main/java/com/github/khakers/modmailviewer/data/User.java b/src/main/java/com/github/khakers/modmailviewer/data/User.java index 0f31f169..fd0e5f4c 100644 --- a/src/main/java/com/github/khakers/modmailviewer/data/User.java +++ b/src/main/java/com/github/khakers/modmailviewer/data/User.java @@ -10,6 +10,10 @@ public record User( @BsonProperty(value = "avatar_url") @JsonProperty("avatar_url") String avatarUrl, + /* + * This is true if the channel the message was sent in was not a DM + * It implies nothing about actual status as a modw + */ boolean mod ) { } From 78803319e49091402a5f8d36810c1a805a59e922 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:42:17 -0700 Subject: [PATCH 2/8] feat: Add non image type attachment support Adds attachment cards for all non-media (image/video) type attachments. Moves videos into --- .../khakers/modmailviewer/jte/JteContext.java | 8 ++ .../util/AttachmentFormatUtils.java | 96 +++++++++++++++++++ src/main/jte/macros/FileAttachment.jte | 22 +++++ src/main/jte/macros/MediaAttachment.jte | 59 ++++++++++++ src/main/jte/macros/imageAttachment.jte | 34 ------- src/main/jte/pages/LogEntryView.jte | 32 +++++-- src/main/resources/static/css/styles.css | 20 +++- 7 files changed, 225 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/github/khakers/modmailviewer/util/AttachmentFormatUtils.java create mode 100644 src/main/jte/macros/FileAttachment.jte create mode 100644 src/main/jte/macros/MediaAttachment.jte delete mode 100644 src/main/jte/macros/imageAttachment.jte diff --git a/src/main/java/com/github/khakers/modmailviewer/jte/JteContext.java b/src/main/java/com/github/khakers/modmailviewer/jte/JteContext.java index 7bf6ca0c..f65d9bad 100644 --- a/src/main/java/com/github/khakers/modmailviewer/jte/JteContext.java +++ b/src/main/java/com/github/khakers/modmailviewer/jte/JteContext.java @@ -4,16 +4,24 @@ import gg.jte.Content; import io.javalin.http.Context; +import java.util.Locale; + public final class JteContext { private static final ThreadLocal context = ThreadLocal.withInitial(JteContext::new); private JteLocalizer localizer; + private final Locale locale = Locale.ENGLISH; + public static void init(Context ctx) { JteContext context = getContext(); //todo language context.localizer = new JteLocalizer(Localizer.getInstance("en")); } + public static Locale getLocale() { + return getContext().locale; + } + public static Content localize(String key) { return getContext().localizer.localize(key); } diff --git a/src/main/java/com/github/khakers/modmailviewer/util/AttachmentFormatUtils.java b/src/main/java/com/github/khakers/modmailviewer/util/AttachmentFormatUtils.java new file mode 100644 index 00000000..3661d335 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/util/AttachmentFormatUtils.java @@ -0,0 +1,96 @@ +package com.github.khakers.modmailviewer.util; + +import com.github.khakers.modmailviewer.data.Attachment; + +public class AttachmentFormatUtils { + + public static boolean isMediaAttachment(Attachment attachment) { + return isImage(attachment) || isVideo(attachment); + } + + public static boolean isImage(Attachment attachment) { + if (attachment.contentType() == null) { + return isImage(getFileExtension(attachment.filename())); + } + return attachment.contentType().startsWith("image"); + } + + public static boolean isVideo(Attachment attachment) { + if (attachment.contentType() == null) { + return isSupportedVideoContainer(getFileExtension(attachment.filename())); + } + return attachment.contentType().startsWith("video"); + } + + public static boolean isAudio(Attachment attachment) { + if (attachment.contentType() == null) { + return isAudio(getFileExtension(attachment.filename())); + } + return attachment.contentType().startsWith("audio"); + } + + public static boolean isImage(String format) { + format = format + .strip() + .replace(".", "") + .toLowerCase(); + return format.equals("png") + || format.equals("jpg") + || format.equals("jpeg") + || format.equals("gif") + || format.equals("webp") + || format.equals("bmp") + || format.equals("tiff") + || format.equals("avif"); + } + + + /** + * Best effort guess at whether the file is an audio file based on the file extension + * + * @param format The file extension string + * @return Whether the file extension appears to be an audio file + */ + public static boolean isAudio(String format) { + format = format + .strip() + .replace(".", "") + .toLowerCase(); + return format.equals("ogg") + || format.equals("oga") + || format.equals("wav") + || format.equals("flac") + || format.equals("mp3") + || format.equals("opus") + || format.equals("pcm") + || format.equals("vorbis") + || format.equals("aac"); + } + + public static boolean isSupportedAudioContainer(String format) { + format = format.toLowerCase(); + return format.equals("ogg") + || format.equals("wav") + || format.equals("flac"); + } + + /** + * Best effort guess at whether the file is a supported video container in chromium based on the file extension + * + * @param format The file extension string + * @return Whether the file extension appears to be supported video container + */ + public static boolean isSupportedVideoContainer(String format) { + format = format + .strip() + .replace(".", "") + .toLowerCase(); + return format.equals("webm") + || format.equals("mp4") + || format.equals("mkv"); + } + + public static String getFileExtension(String filename) { + return filename.substring(filename.lastIndexOf(".") + 1); + } +} diff --git a/src/main/jte/macros/FileAttachment.jte b/src/main/jte/macros/FileAttachment.jte new file mode 100644 index 00000000..71c65edc --- /dev/null +++ b/src/main/jte/macros/FileAttachment.jte @@ -0,0 +1,22 @@ +@import com.github.khakers.modmailviewer.data.Attachment +@import com.github.khakers.modmailviewer.data.Message +@import com.github.khakers.modmailviewer.util.AttachmentFormatUtils +@import java.text.NumberFormat + +@param Attachment attachment +@param Message message +@param boolean nsfw +@param NumberFormat numberFormat + + +
+
+
${attachment.filename()}
+
${attachment.size()}
+ @if(AttachmentFormatUtils.isAudio(attachment)) + + @elseif(AttachmentFormatUtils.isVideo(attachment)) + + @endif +
+
\ No newline at end of file diff --git a/src/main/jte/macros/MediaAttachment.jte b/src/main/jte/macros/MediaAttachment.jte new file mode 100644 index 00000000..88c11c70 --- /dev/null +++ b/src/main/jte/macros/MediaAttachment.jte @@ -0,0 +1,59 @@ +@import com.github.khakers.modmailviewer.data.Attachment +@import com.github.khakers.modmailviewer.data.Message +@import com.github.khakers.modmailviewer.util.AttachmentFormatUtils + +@param Attachment attachment +@param Message message +@param boolean nsfw + +@if(attachment.filename().startsWith("SPOILER_")) +
+ + @if(AttachmentFormatUtils.isImage(attachment)) + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else + + @endif + +
+@elseif(nsfw) +
+ + @if(AttachmentFormatUtils.isImage(attachment)) + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else + + @endif + +
+@else +
+ @if(AttachmentFormatUtils.isImage(attachment)) + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else + + @endif +
+ +@endif + diff --git a/src/main/jte/macros/imageAttachment.jte b/src/main/jte/macros/imageAttachment.jte deleted file mode 100644 index 65f5ea1e..00000000 --- a/src/main/jte/macros/imageAttachment.jte +++ /dev/null @@ -1,34 +0,0 @@ -@import com.github.khakers.modmailviewer.data.Attachment -@import com.github.khakers.modmailviewer.data.Message - -@param Attachment attachment -@param Message message -@param boolean nsfw - -@if(attachment.filename().startsWith("SPOILER_")) -
- - Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} - -
-@elseif(nsfw) -
- - Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} - -
-@else -
- Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} -
-@endif - diff --git a/src/main/jte/pages/LogEntryView.jte b/src/main/jte/pages/LogEntryView.jte index 6d9ba2dc..b9764b5e 100644 --- a/src/main/jte/pages/LogEntryView.jte +++ b/src/main/jte/pages/LogEntryView.jte @@ -1,5 +1,6 @@ @import com.github.khakers.modmailviewer.Main @import com.github.khakers.modmailviewer.data.MessageType +@import com.github.khakers.modmailviewer.util.AttachmentFormatUtils @import com.github.khakers.modmailviewer.util.DiscordUtils @import static com.github.khakers.modmailviewer.jte.JteContext.* @@ -8,6 +9,7 @@ !{var parser = Main.PARSER;} !{var renderer = Main.RENDERER;} !{var modmailLog = page.log;} +!{var nf = java.text.NumberFormat.getInstance(getLocale());} @template.layout.Page(page = page, content = @`
@@ -71,11 +73,16 @@ $unsafe{renderer.render(parser.parse(message.get().getContent()))}
@if(!message.get().getAttachments().isEmpty()) - @for(var attachment: message.get().getAttachments()) - @if(attachment.isImage()) - @template.macros.imageAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) - @endif - @endfor +
+ @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) + @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) + @endfor +
+
+ @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList()) + @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf) + @endfor +
@endif @@ -141,11 +148,16 @@ (${localize("MESSAGE_EDITED_FLAG")}) @endif @if(!message.get().getAttachments().isEmpty()) - @for(var attachment: message.get().getAttachments()) - @if(attachment.isImage()) - @template.macros.imageAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) - @endif - @endfor +
+ @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) + @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) + @endfor +
+
+ @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList()) + @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf) + @endfor +
@endif diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index cbaaee5d..80d4cedf 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -90,7 +90,7 @@ div.spoilerImage { padding: 0; } -div.spoilerImage img { +div.spoilerImage img, div.spoilerImage video { vertical-align: bottom; filter: blur(30px); transition: 0.3s; @@ -130,7 +130,7 @@ div.spoilerImage input { height: 0; } -div.spoilerImage input:checked ~ img { +div.spoilerImage input:checked ~ img, div.spoilerImage input:checked ~ video { filter: none; } @@ -266,6 +266,22 @@ up-modal-box { vertical-align: .125em; } +/*.media-attachments {*/ +/* width: 18rem;*/ +/*}*/ +.non-media-attachments { + max-width: 48rem; +} +.attachment-card+.attachment-card { + margin-top: .5rem; +} + +.attachment-card video { + width: 100%; + height: 100%; + object-fit: contain; +} + .chart { height: 400px; max-height: 480px; From 34d53121f486735e2cff6a59c0dcbb58155ca715 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Sun, 30 Jul 2023 21:05:46 -0700 Subject: [PATCH 3/8] feat: Add cdn.discordapp.com to media csp --- src/main/java/com/github/khakers/modmailviewer/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index c6d022ec..0a815cbf 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -350,7 +350,7 @@ private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConf "default-src 'self'; " + "img-src * 'self' data:; " + "object-src 'none'; " + - "media-src media.discordapp.com; " + + "media-src media.discordapp.com cdn.discordapp.com; " + "style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog='; " + "script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.0/dist/twemoji.min.js %s;", cspConfig.extraScriptSources().orElse(""))); From 6ad4e9271647e5b4f9ad57a95f720c526c89ea9e Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Tue, 1 Aug 2023 14:45:54 -0700 Subject: [PATCH 4/8] refactor: improve video and audio sizing and add media attachments to grid In the future it would be nice to add some logic to the image grid like how discord does it. It would likely require a lightbox which doesn't work properly at the moment. --- src/main/jte/pages/LogEntryView.jte | 12 ++++++++---- src/main/resources/static/css/styles.css | 23 ++++++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/jte/pages/LogEntryView.jte b/src/main/jte/pages/LogEntryView.jte index b9764b5e..61b809d5 100644 --- a/src/main/jte/pages/LogEntryView.jte +++ b/src/main/jte/pages/LogEntryView.jte @@ -73,9 +73,11 @@ $unsafe{renderer.render(parser.parse(message.get().getContent()))} @if(!message.get().getAttachments().isEmpty()) -
+
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) - @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) +
+ @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) +
@endfor
@@ -148,9 +150,11 @@ (${localize("MESSAGE_EDITED_FLAG")}) @endif @if(!message.get().getAttachments().isEmpty()) -
+
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) - @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) +
+ @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) +
@endfor
diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index 80d4cedf..1d1eb113 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -77,10 +77,10 @@ img.emoji { vertical-align: -0.1em; } -.image { - max-width: 256px; - max-height: 256px; -} +/*.image {*/ +/* max-width: 256px;*/ +/* max-height: 256px;*/ +/*}*/ div.spoilerImage { @@ -266,12 +266,17 @@ up-modal-box { vertical-align: .125em; } -/*.media-attachments {*/ -/* width: 18rem;*/ -/*}*/ +.media-attachments { + max-width: 48rem; +} .non-media-attachments { max-width: 48rem; } + +.media-attachments video, .media-attachments image { + max-height: 350px; +} + .attachment-card+.attachment-card { margin-top: .5rem; } @@ -282,6 +287,10 @@ up-modal-box { object-fit: contain; } +.attachment-card audio { + width: 100%; +} + .chart { height: 400px; max-height: 480px; From 1992a0a8b490fb587cc07808d0521660c5278f10 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:00:17 -0700 Subject: [PATCH 5/8] feat: Add image lightbox using Glightbox Integrates Glightbox into thread images and videos. Disables pointer events on spoilered image Reformats the CSP headers declaration and adds some inline styles that glightbox uses as well as discord cdn/media URLs to frame-src Galleries do not yet work, they may need to be manually set in JS with a compiler or such --- build.gradle | 2 + gradle/libs.versions.toml | 5 +- .../github/khakers/modmailviewer/Main.java | 6 +- src/main/jte/macros/HeaderImports.jte | 5 ++ src/main/jte/macros/MediaAttachment.jte | 65 ++++++++++++------- src/main/jte/pages/LogEntryView.jte | 4 +- src/main/resources/static/css/styles.css | 11 +++- src/main/resources/static/js/BaseModules.js | 17 +++++ 8 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 src/main/resources/static/js/BaseModules.js diff --git a/build.gradle b/build.gradle index 7919ea69..90bec86a 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,8 @@ dependencies { implementation libs.webjar.chartjs implementation libs.webjar.chartjs.plugin.autocolors implementation libs.webjar.humanize.duration + implementation(libs.webjar.glightbox) + implementation(libs.webjar.plyr) implementation libs.mongodb.driver implementation libs.mongojack diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d60aaed0..53a0147a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,8 @@ unpoly = "3.3.0" chartjs = "4.2.1" chartjs-plugin-autocolors = "0.2.2" humanize-duration = "3.28.0" +glightbox = "3.2.0" +plyr = "3.7.8" [libraries] javalin = { module = "io.javalin:javalin", version.ref = "javalin" } @@ -78,7 +80,8 @@ webjar-unpoly = { module = "org.webjars.npm:unpoly", version.ref = "unpoly" } webjar-chartjs = { module = "org.webjars.npm:chart.js", version.ref = "chartjs" } webjar-chartjs-plugin-autocolors = { module = "org.webjars.npm:chartjs-plugin-autocolors", version.ref = "chartjs-plugin-autocolors" } webjar-humanize-duration = { module = "org.webjars.npm:humanize-duration", version.ref = "humanize-duration" } - +webjar-glightbox = { module = "org.webjars.npm:glightbox", version.ref = "glightbox" } +webjar-plyr = { module = "org.webjars.npm:plyr", version.ref = "plyr" } [plugins] jte-gradle = { id = "gg.jte.gradle", version.ref = "jte" } diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 0a815cbf..8f53e95e 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -347,11 +347,13 @@ private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConf globalHeaderConfig.contentSecurityPolicy(cspConfig.override().get()); } else { globalHeaderConfig.contentSecurityPolicy(String.format( - "default-src 'self'; " + + "default-src 'self'; " + "img-src * 'self' data:; " + "object-src 'none'; " + "media-src media.discordapp.com cdn.discordapp.com; " + - "style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog='; " + + "style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog=' 'sha256-ubXkvHkNI/o3njlOwWcW1Nrt3/3G2eJn8mN1u9LCnXo='; " + + "style-src 'self' 'sha256-Jt4TB/uiervjq+0TSAyeKjWbMJlLUrE4uXVVOyC/xQA='; "+ + "frame-src 'self' https://cdn.discordapp.com https://media.discordapp.com; " + "script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.0/dist/twemoji.min.js %s;", cspConfig.extraScriptSources().orElse(""))); } diff --git a/src/main/jte/macros/HeaderImports.jte b/src/main/jte/macros/HeaderImports.jte index 6a32c3bd..99ea3128 100644 --- a/src/main/jte/macros/HeaderImports.jte +++ b/src/main/jte/macros/HeaderImports.jte @@ -31,6 +31,11 @@ + + + + + @if(com.github.khakers.modmailviewer.configuration.Config.appConfig.analytics().isPresent())- $unsafe{com.github.khakers.modmailviewer.configuration.Config.appConfig.analytics().get()} diff --git a/src/main/jte/macros/MediaAttachment.jte b/src/main/jte/macros/MediaAttachment.jte index 88c11c70..119b32e3 100644 --- a/src/main/jte/macros/MediaAttachment.jte +++ b/src/main/jte/macros/MediaAttachment.jte @@ -6,21 +6,27 @@ @param Message message @param boolean nsfw +<%--'data-type="video"' must be properly set, otherwise, some videos will download instead of opening the lightbox --%> + @if(attachment.filename().startsWith("SPOILER_"))
@if(AttachmentFormatUtils.isImage(attachment)) - Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else - + + + @endif
@@ -28,17 +34,21 @@
@if(AttachmentFormatUtils.isImage(attachment)) - Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else - + + + @endif @@ -46,12 +56,17 @@ @else
@if(AttachmentFormatUtils.isImage(attachment)) - Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} + @else - + + <%-- potato--%> + + @endif
diff --git a/src/main/jte/pages/LogEntryView.jte b/src/main/jte/pages/LogEntryView.jte index 61b809d5..9ce876a6 100644 --- a/src/main/jte/pages/LogEntryView.jte +++ b/src/main/jte/pages/LogEntryView.jte @@ -73,7 +73,7 @@ $unsafe{renderer.render(parser.parse(message.get().getContent()))}
@if(!message.get().getAttachments().isEmpty()) -
+
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())
@template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) @@ -150,7 +150,7 @@ (${localize("MESSAGE_EDITED_FLAG")}) @endif @if(!message.get().getAttachments().isEmpty()) -
+
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())
@template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index 1d1eb113..a4a321d0 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -90,12 +90,19 @@ div.spoilerImage { padding: 0; } -div.spoilerImage img, div.spoilerImage video { +div.spoilerImage a img, div.spoilerImage a video { vertical-align: bottom; filter: blur(30px); transition: 0.3s; } +/* + So lightboxes don't get opened on images that are still hidden by spoilers (or nsfw) + */ +div.spoilerImage input:not(:checked) ~ a { + pointer-events: none; +} + .spoilerImageButton { position: absolute; top: 0; @@ -130,7 +137,7 @@ div.spoilerImage input { height: 0; } -div.spoilerImage input:checked ~ img, div.spoilerImage input:checked ~ video { +div.spoilerImage input:checked ~ a img, div.spoilerImage input:checked ~ a video { filter: none; } diff --git a/src/main/resources/static/js/BaseModules.js b/src/main/resources/static/js/BaseModules.js new file mode 100644 index 00000000..8d1c85ed --- /dev/null +++ b/src/main/resources/static/js/BaseModules.js @@ -0,0 +1,17 @@ +"use strict"; + +import '/webjars/glightbox/3.2.0/dist/js/glightbox.min.js'; + +const CSS_URL = new URL("/webjars/plyr/3.7.8/dist/plyr.css",window.location.origin); +const JS_URL = new URL("/webjars/plyr/3.7.8/dist/plyr.min.js",window.location.origin); +const ICON_URL = new URL("/webjars/plyr/3.7.8/dist/plyr.svg",window.location.origin); + +const lightbox = GLightbox({ + plyr: { + css: CSS_URL.toString(), + js: JS_URL.toString(), + config: { + iconUrl: ICON_URL.toString(), + } + } +}); From ee41389e6cf89da77d7302cb82c8b202aafb5028 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:16:53 -0700 Subject: [PATCH 6/8] fix: reload lightbox when unpoly inserts a new fragment. --- src/main/resources/static/js/BaseModules.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/static/js/BaseModules.js b/src/main/resources/static/js/BaseModules.js index 8d1c85ed..93dbe077 100644 --- a/src/main/resources/static/js/BaseModules.js +++ b/src/main/resources/static/js/BaseModules.js @@ -15,3 +15,8 @@ const lightbox = GLightbox({ } } }); + + +up.on("up:fragment:inserted", () => { + lightbox.reload(); +}); \ No newline at end of file From 17dad529feba903ca9bf0b163ba1f2217d3a6593 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:27:51 -0700 Subject: [PATCH 7/8] fix: properly configure individual lightbox galleries per message. Each message has a gallery id of the index of the message. Each message should be its own gallery, so you can't accidentally page to a different messages attachments. Mirrors discord behavior. --- src/main/jte/macros/MediaAttachment.jte | 13 +++++++------ src/main/jte/pages/LogEntryView.jte | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/jte/macros/MediaAttachment.jte b/src/main/jte/macros/MediaAttachment.jte index 119b32e3..76ad19bd 100644 --- a/src/main/jte/macros/MediaAttachment.jte +++ b/src/main/jte/macros/MediaAttachment.jte @@ -5,6 +5,7 @@ @param Attachment attachment @param Message message @param boolean nsfw +@param int galleryIndex = 0 <%--'data-type="video"' must be properly set, otherwise, some videos will download instead of opening the lightbox --%> @@ -12,14 +13,14 @@
@if(AttachmentFormatUtils.isImage(attachment)) - + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} @else - + + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} @else - + + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} @else - + <%-- potato--%> diff --git a/src/main/jte/pages/LogEntryView.jte b/src/main/jte/pages/LogEntryView.jte index 9ce876a6..1cfab0cf 100644 --- a/src/main/jte/pages/LogEntryView.jte +++ b/src/main/jte/pages/LogEntryView.jte @@ -76,7 +76,7 @@
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())
- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) + @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex())
@endfor
@@ -153,7 +153,7 @@
@for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())
- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw()) + @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex())
@endfor
From bb0f29e832e933b48168d6d6b1ccdf7cd2a25007 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Fri, 9 Feb 2024 22:21:47 -0800 Subject: [PATCH 8/8] feat: add s3 and mongodb attachment handling --- .../github/khakers/modmailviewer/Main.java | 57 ++++-------------- .../attachments/AttachmentClient.java | 5 ++ .../AttachmentNotFoundException.java | 7 +++ .../attachments/AttachmentResult.java | 8 +++ .../attachments/MongoAttachment.java | 30 ++++++++++ .../attachments/MongoAttachmentClient.java | 28 +++++++++ .../UnsupportedAttachmentException.java | 10 ++++ .../configuration/AppConfig.java | 4 +- .../modmailviewer/configuration/Config.java | 20 ++++--- .../InvalidConfigurationException.java | 22 +++++++ .../modmailviewer/data/Attachment.java | 51 +++++++++++++++- .../page/attachment/AttachmentController.java | 49 +++++++++++++++ src/main/jte/macros/AttachmentsContainer.jte | 25 ++++++++ src/main/jte/macros/FileAttachment.jte | 13 ++-- src/main/jte/macros/MediaAttachment.jte | 22 +++---- src/main/jte/pages/LogEntryView.jte | 59 ++++++++++--------- 16 files changed, 310 insertions(+), 100 deletions(-) create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentClient.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentNotFoundException.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentResult.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachment.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachmentClient.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/attachments/UnsupportedAttachmentException.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/InvalidConfigurationException.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/page/attachment/AttachmentController.java create mode 100644 src/main/jte/macros/AttachmentsContainer.jte diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 8f53e95e..ec0a71f4 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -1,6 +1,7 @@ package com.github.khakers.modmailviewer; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.github.khakers.modmailviewer.attachments.MongoAttachmentClient; import com.github.khakers.modmailviewer.auditlog.AuditEventDAO; import com.github.khakers.modmailviewer.auditlog.MongoAuditEventLogger; import com.github.khakers.modmailviewer.auditlog.NoopAuditEventLogger; @@ -9,6 +10,7 @@ import com.github.khakers.modmailviewer.auth.Role; import com.github.khakers.modmailviewer.configuration.AppConfig; import com.github.khakers.modmailviewer.configuration.CSPConfig; +import com.github.khakers.modmailviewer.configuration.Config; import com.github.khakers.modmailviewer.configuration.SSLConfig; import com.github.khakers.modmailviewer.log.LogController; import com.github.khakers.modmailviewer.markdown.channelmention.ChannelMentionExtension; @@ -18,6 +20,7 @@ import com.github.khakers.modmailviewer.markdown.underline.UnderlineExtension; import com.github.khakers.modmailviewer.markdown.usermention.UserMentionExtension; import com.github.khakers.modmailviewer.page.admin.AdminController; +import com.github.khakers.modmailviewer.page.attachment.AttachmentController; import com.github.khakers.modmailviewer.page.audit.AuditController; import com.github.khakers.modmailviewer.page.dashboard.DashboardController; import com.github.khakers.modmailviewer.page.dashboard.MetricsAccessor; @@ -49,13 +52,7 @@ import org.apache.logging.log4j.Logger; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.pojo.PojoCodecProvider; -import org.github.gestalt.config.Gestalt; -import org.github.gestalt.config.builder.GestaltBuilder; import org.github.gestalt.config.exceptions.GestaltException; -import org.github.gestalt.config.path.mapper.SnakeCasePathMapper; -import org.github.gestalt.config.source.ClassPathConfigSourceBuilder; -import org.github.gestalt.config.source.EnvironmentConfigSourceBuilder; -import org.github.gestalt.config.source.SystemPropertiesConfigSourceBuilder; import org.jetbrains.annotations.NotNull; import java.net.URI; @@ -102,47 +99,13 @@ public class Main { public static void main(String[] args) throws GestaltException { - Gestalt gestalt = new GestaltBuilder() - .setTreatNullValuesInClassAsErrors(true) - .setTreatMissingValuesAsErrors(false) - .addSource(ClassPathConfigSourceBuilder.builder() - .setResource("/default.properties") - .build()) // Load the default property files from resources. - .addSource(EnvironmentConfigSourceBuilder.builder() - .setPrefix(envPrepend) - .setRemovePrefix(true) - .build()) - .addSource(SystemPropertiesConfigSourceBuilder.builder() - .setFailOnErrors(false) - .build()) - .addDefaultPathMappers() - .addPathMapper(new SnakeCasePathMapper()) - .build(); - - try { - gestalt.loadConfigs(); - } catch (GestaltException e) { - logger.fatal(e); - System.exit(1); - throw new RuntimeException(); - } - - AppConfig appConfigInit; - try { - appConfigInit = gestalt.getConfig("app", AppConfig.class); - } catch (GestaltException e) { - logger.fatal(e); - System.exit(1); - throw new RuntimeException(); - } - var appConfig = appConfigInit; + var appConfig = Config.appConfig; logger.debug(appConfig.toString()); var auditLogConfig = appConfig.auditLogConfig(); // var cspConfig = appConfig.cspConfig(); var authConfig = appConfig.auth().orElse(null); - TemplateEngine templateEngine; if (appConfig.dev()) { @@ -206,6 +169,7 @@ public static void main(String[] args) throws GestaltException { var logController = new LogController(auditLogger); var dashboardController = new DashboardController(); + var attachmentController = new AttachmentController(new MongoAttachmentClient(mongoClientDatabase)); var app = Javalin.create(javalinConfig -> { try { @@ -237,6 +201,7 @@ public static void main(String[] args) throws GestaltException { .get("/dashboard", dashboardController.serveDashboardPage, RoleUtils.atLeastSupporter()) .get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator()) .get("/audit/{id}", auditController.serveAuditPage, RoleUtils.atLeastAdministrator()) + .get("/attachment/{id}", attachmentController.getHandler(), RoleUtils.atLeastSupporter()) .after("/api/*", ctx -> { if (auditLogConfig.isApiAuditingEnabled()) { if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { @@ -274,7 +239,7 @@ private static void configure(JavalinConfig config, AppConfig appConfig) throws config.showJavalinBanner = false; config.jsonMapper(new JavalinJackson().updateMapper(objectMapper -> objectMapper.registerModule(new Jdk8Module()))); - config.plugins.enableGlobalHeaders(() -> configureHeaders(sslOptions.get(), cspConfig)); + config.plugins.enableGlobalHeaders(() -> configureHeaders(sslOptions.get(), cspConfig, appConfig)); if (sslOptions.isPresent() && sslOptions.get().httpsOnly()) { logger.info("HTTPS only is ENABLED"); config.plugins.enableSslRedirects(); @@ -333,7 +298,7 @@ private static SSLPlugin getSslPlugin(AppConfig appConfig, SSLConfig sslOptions) }); } - private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConfig cspConfig) { + private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConfig cspConfig, AppConfig appConfig) { var globalHeaderConfig = new GlobalHeaderConfig(); globalHeaderConfig.xFrameOptions(GlobalHeaderConfig.XFrameOptions.DENY); globalHeaderConfig.xContentTypeOptionsNoSniff(); @@ -348,12 +313,12 @@ private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConf } else { globalHeaderConfig.contentSecurityPolicy(String.format( "default-src 'self'; " + - "img-src * 'self' data:; " + + "img-src * 'self' data: "+appConfig.s3Url().orElse("")+"; " + "object-src 'none'; " + - "media-src media.discordapp.com cdn.discordapp.com; " + + "media-src 'self' media.discordapp.com cdn.discordapp.com "+appConfig.s3Url().orElse("")+"; " + "style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog=' 'sha256-ubXkvHkNI/o3njlOwWcW1Nrt3/3G2eJn8mN1u9LCnXo='; " + "style-src 'self' 'sha256-Jt4TB/uiervjq+0TSAyeKjWbMJlLUrE4uXVVOyC/xQA='; "+ - "frame-src 'self' https://cdn.discordapp.com https://media.discordapp.com; " + + "frame-src 'self' https://cdn.discordapp.com https://media.discordapp.com "+appConfig.s3Url().orElse("")+"; " + "script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.0/dist/twemoji.min.js %s;", cspConfig.extraScriptSources().orElse(""))); } diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentClient.java b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentClient.java new file mode 100644 index 00000000..566921df --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentClient.java @@ -0,0 +1,5 @@ +package com.github.khakers.modmailviewer.attachments; + +public interface AttachmentClient { + AttachmentResult getAttachment(long id) throws AttachmentNotFoundException, UnsupportedAttachmentException; +} diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentNotFoundException.java b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentNotFoundException.java new file mode 100644 index 00000000..5104cd24 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentNotFoundException.java @@ -0,0 +1,7 @@ +package com.github.khakers.modmailviewer.attachments; + +public class AttachmentNotFoundException extends Exception { + public AttachmentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentResult.java b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentResult.java new file mode 100644 index 00000000..80f40d33 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/AttachmentResult.java @@ -0,0 +1,8 @@ +package com.github.khakers.modmailviewer.attachments; + +public record AttachmentResult( + byte[] attachmentData, + String content_type + +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachment.java b/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachment.java new file mode 100644 index 00000000..4416b15a --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachment.java @@ -0,0 +1,30 @@ +package com.github.khakers.modmailviewer.attachments; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.bson.codecs.pojo.annotations.BsonProperty; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +@JsonIgnoreProperties(ignoreUnknown = true) +public record MongoAttachment( + @BsonProperty("_id") + @JsonProperty("_id") + long id, + @BsonProperty("content_type") + @JsonProperty("content_type") + String contentType, + byte[] data, +// @Nullable String description, + String filename, + int size, + @Nullable + Integer height, + @Nullable + Integer width, + @BsonProperty("uploaded_at") + @JsonProperty("uploaded_at") + Instant uploadTime + +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachmentClient.java b/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachmentClient.java new file mode 100644 index 00000000..ca7b9baa --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/MongoAttachmentClient.java @@ -0,0 +1,28 @@ +package com.github.khakers.modmailviewer.attachments; + +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import org.bson.UuidRepresentation; +import org.mongojack.JacksonMongoCollection; + +public class MongoAttachmentClient implements AttachmentClient { + protected MongoDatabase mongoDatabase; + protected JacksonMongoCollection collection; + + public MongoAttachmentClient(MongoDatabase mongoDatabase) { + this.mongoDatabase = mongoDatabase; + // I wanted to use the mongodb java driver's pojo support, but it doesn't support integer types being null... + // So I'm using mongojack instead + this.collection = JacksonMongoCollection.builder().build(mongoDatabase, "attachments", MongoAttachment.class, UuidRepresentation.STANDARD); + } + + @Override + public AttachmentResult getAttachment(long id) throws AttachmentNotFoundException { + var result = collection.find(Filters.eq("_id", id)).first(); + if (result == null) { + throw new AttachmentNotFoundException("attachment of id " + id + " not found"); + } + + return new AttachmentResult(result.data(), result.contentType()); + } +} diff --git a/src/main/java/com/github/khakers/modmailviewer/attachments/UnsupportedAttachmentException.java b/src/main/java/com/github/khakers/modmailviewer/attachments/UnsupportedAttachmentException.java new file mode 100644 index 00000000..3f6e5b10 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/attachments/UnsupportedAttachmentException.java @@ -0,0 +1,10 @@ +package com.github.khakers.modmailviewer.attachments; + +/** + * The attachment was found, but does not contain all the data required to properly return it. + */ +public class UnsupportedAttachmentException extends Exception{ + public UnsupportedAttachmentException(String message) { + super(message); + } +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java index 0498e028..fdf41716 100644 --- a/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java @@ -30,6 +30,8 @@ public record AppConfig( CSPConfig cspConfig, long botId, String branding, - Optional analytics + Optional analytics, + + Optional s3Url ) { } diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java b/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java index 2d78d88c..576d6a39 100644 --- a/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java @@ -7,10 +7,7 @@ import org.github.gestalt.config.builder.GestaltBuilder; import org.github.gestalt.config.exceptions.GestaltException; import org.github.gestalt.config.path.mapper.SnakeCasePathMapper; -import org.github.gestalt.config.source.ClassPathConfigSource; -import org.github.gestalt.config.source.EnvironmentConfigSource; -import org.github.gestalt.config.source.FileConfigSource; -import org.github.gestalt.config.source.SystemPropertiesConfigSource; +import org.github.gestalt.config.source.*; import java.io.File; @@ -26,7 +23,9 @@ public class Config { var gestaltBuilder = new GestaltBuilder() .setTreatNullValuesInClassAsErrors(true) .setTreatMissingValuesAsErrors(false) - .addSource(new ClassPathConfigSource("default.properties")); + .addSource(ClassPathConfigSourceBuilder.builder() + .setResource("/default.properties") + .build()); // Load the default property files from resources. if (configURI != null) { File file = new File(configURI); @@ -40,11 +39,16 @@ public class Config { } - gestalt = gestaltBuilder.addSource(new EnvironmentConfigSource(Main.envPrepend)) - .addSource(new SystemPropertiesConfigSource()) + gestalt = gestaltBuilder + .addSource(EnvironmentConfigSourceBuilder.builder() + .setPrefix(Main.envPrepend) + .setRemovePrefix(true) + .build()) + .addSource(SystemPropertiesConfigSourceBuilder.builder() + .setFailOnErrors(false) + .build()) .addDefaultPathMappers() .addPathMapper(new SnakeCasePathMapper()) - // .addSource(new FileConfigSource(devFile)) .build(); } catch (GestaltException e) { diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/InvalidConfigurationException.java b/src/main/java/com/github/khakers/modmailviewer/configuration/InvalidConfigurationException.java new file mode 100644 index 00000000..b21964a6 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/InvalidConfigurationException.java @@ -0,0 +1,22 @@ +package com.github.khakers.modmailviewer.configuration; + +/** + * Thrown when the supplied configuration is invalid + */ +public class InvalidConfigurationException extends Exception { + public InvalidConfigurationException(String message) { + super(message); + } + + public InvalidConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidConfigurationException(Throwable cause) { + super(cause); + } + + public InvalidConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java b/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java index 4b9388d7..57d20caa 100644 --- a/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java +++ b/src/main/java/com/github/khakers/modmailviewer/data/Attachment.java @@ -1,23 +1,70 @@ package com.github.khakers.modmailviewer.data; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.khakers.modmailviewer.configuration.Config; +import org.apache.logging.log4j.LogManager; import org.bson.BsonType; +import org.bson.codecs.pojo.annotations.BsonProperty; import org.bson.codecs.pojo.annotations.BsonRepresentation; import org.jetbrains.annotations.Nullable; +import java.util.Optional; + + public record Attachment( @BsonRepresentation(BsonType.STRING) long id, String filename, String url, - //isImage does not correctly Identify whether this is an image @JsonProperty("is_image") + @BsonProperty("is_image") + @Deprecated boolean isImage, int size, + Optional type, + //Requires modmail enhanced feature support @JsonProperty("content_type") + @BsonProperty("content_type") + @Nullable + String contentType, + + @JsonProperty("s3_object") + @BsonProperty("s3_object") + @Nullable + String s3Object, + @JsonProperty("s3_bucket") + @BsonProperty("s3_bucket") @Nullable - String contentType + String s3Bucket + ) { + + public Optional getAttachmentURI() { + if (type.isPresent() && type.get().equals("internal")) { + return Optional.of("/attachment/" + id); + } + if (type.isPresent() && type.get().equals("s3")) { + if (Config.appConfig.s3Url().isEmpty()) { + LogManager.getLogger().error("S3 URL not configured, cannot generate attachment URL"); + return Optional.empty(); + } + if (s3Bucket == null || s3Object == null) { + LogManager.getLogger().error("The attachment {} is missing s3 bucket ({}) or object ({}) information, cannot generate attachment URL", id, s3Bucket, s3Object); + return Optional.empty(); + } + return (Config.appConfig.s3Url().get()+"/"+ s3Bucket+"/"+ s3Object).describeConstable(); + } + + return url.describeConstable(); + } + + public boolean isImageContentType() { + return contentType != null && contentType.startsWith("image/"); + } + + public boolean isVideoContentType() { + return contentType != null && contentType.startsWith("video/"); + } } diff --git a/src/main/java/com/github/khakers/modmailviewer/page/attachment/AttachmentController.java b/src/main/java/com/github/khakers/modmailviewer/page/attachment/AttachmentController.java new file mode 100644 index 00000000..4279845f --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/page/attachment/AttachmentController.java @@ -0,0 +1,49 @@ +package com.github.khakers.modmailviewer.page.attachment; + + +import com.github.khakers.modmailviewer.attachments.AttachmentClient; +import com.github.khakers.modmailviewer.attachments.AttachmentNotFoundException; +import com.github.khakers.modmailviewer.attachments.UnsupportedAttachmentException; +import io.javalin.http.Context; +import io.javalin.http.Handler; +import io.javalin.http.InternalServerErrorResponse; +import io.javalin.http.NotFoundResponse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class AttachmentController { + private static final Logger logger = LogManager.getLogger(); + + protected AttachmentClient handler; + + public AttachmentController(AttachmentClient handler) { + this.handler = handler; + } + + public void handle (Context ctx) { + logger.traceEntry(); + var id = ctx.pathParamAsClass("id", Long.class).get(); + logger.debug("getting attachment id {}", id); + try { + var attachment_data = handler.getAttachment(id); + + ctx.contentType(attachment_data.content_type()); + ctx.result(attachment_data.attachmentData()); + // set caching headers + ctx.header("max-age", "604800").header("immutable",""); + // Override X-Frame-Options header + // This is required for the attachment viewer to work + ctx.header("X-Frame-Options", "SAMEORIGIN"); + } catch (AttachmentNotFoundException e) { + throw new NotFoundResponse(); + } catch (UnsupportedAttachmentException e) { + logger.throwing(e); + throw new InternalServerErrorResponse(""); + } + logger.traceExit(); + + } + public Handler getHandler() { + return this::handle; + } +} diff --git a/src/main/jte/macros/AttachmentsContainer.jte b/src/main/jte/macros/AttachmentsContainer.jte new file mode 100644 index 00000000..17d9a1cd --- /dev/null +++ b/src/main/jte/macros/AttachmentsContainer.jte @@ -0,0 +1,25 @@ +@import java.util.List; +@import com.github.khakers.modmailviewer.data.Attachment +@import com.github.khakers.modmailviewer.data.Message +@import com.github.khakers.modmailviewer.util.AttachmentFormatUtils; + +@param List attachments +@param gg.jte.support.ForSupport message +@param boolean nsfw = false +@param java.text.NumberFormat numberFormat = java.text.NumberFormat.getInstance() + + +@if(attachments != null && !attachments.isEmpty()) +
+ @for(var attachment: attachments.stream().filter(AttachmentFormatUtils::isMediaAttachment).filter(attachment -> attachment.getAttachmentURI().isPresent()).toList()) +
+ @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = nsfw, galleryIndex = message.getIndex()) +
+ @endfor +
+
+ @for(var attachment: attachments.stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList()) + @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = nsfw, numberFormat = numberFormat) + @endfor +
+@endif \ No newline at end of file diff --git a/src/main/jte/macros/FileAttachment.jte b/src/main/jte/macros/FileAttachment.jte index 71c65edc..60dbf17d 100644 --- a/src/main/jte/macros/FileAttachment.jte +++ b/src/main/jte/macros/FileAttachment.jte @@ -11,12 +11,17 @@
-
${attachment.filename()}
-
${attachment.size()}
+ !{var uri = attachment.getAttachmentURI().orElse(null);} +
${attachment.filename()}
+ @if(attachment.getAttachmentURI().isEmpty()) + + Attachment not found + @endif +
${attachment.size()} Bytes
@if(AttachmentFormatUtils.isAudio(attachment)) - + @elseif(AttachmentFormatUtils.isVideo(attachment)) - + @endif
\ No newline at end of file diff --git a/src/main/jte/macros/MediaAttachment.jte b/src/main/jte/macros/MediaAttachment.jte index 76ad19bd..4fcd064f 100644 --- a/src/main/jte/macros/MediaAttachment.jte +++ b/src/main/jte/macros/MediaAttachment.jte @@ -8,21 +8,21 @@ @param int galleryIndex = 0 <%--'data-type="video"' must be properly set, otherwise, some videos will download instead of opening the lightbox --%> - +!{var uri = attachment.getAttachmentURI().get();} @if(attachment.filename().startsWith("SPOILER_"))
@if(AttachmentFormatUtils.isImage(attachment)) - + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} @else
@if(AttachmentFormatUtils.isImage(attachment)) - + Image ${attachment.filename()} uploaded by ${message.getAuthor().name()} @else diff --git a/src/main/jte/pages/LogEntryView.jte b/src/main/jte/pages/LogEntryView.jte index 1cfab0cf..add753e7 100644 --- a/src/main/jte/pages/LogEntryView.jte +++ b/src/main/jte/pages/LogEntryView.jte @@ -72,20 +72,21 @@
$unsafe{renderer.render(parser.parse(message.get().getContent()))}
- @if(!message.get().getAttachments().isEmpty()) -
- @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) -
- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex()) -
- @endfor -
-
- @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList()) - @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf) - @endfor -
- @endif + @template.macros.AttachmentsContainer(attachments = message.get().getAttachments(), message = message) +<%-- @if(message.get().getAttachments() != null && !message.get().getAttachments().isEmpty())--%> +<%--
--%> +<%-- @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())--%> +<%--
--%> +<%-- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex())--%> +<%--
--%> +<%-- @endfor--%> +<%--
--%> +<%--
--%> +<%-- @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList())--%> +<%-- @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf)--%> +<%-- @endfor--%> +<%--
--%> +<%-- @endif--%>
@@ -149,20 +150,22 @@ @if(message.get().isEdited()) (${localize("MESSAGE_EDITED_FLAG")}) @endif - @if(!message.get().getAttachments().isEmpty()) -
- @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList()) -
- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex()) -
- @endfor -
-
- @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList()) - @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf) - @endfor -
- @endif + @template.macros.AttachmentsContainer(attachments = message.get().getAttachments(), message = message) + +<%-- @if(message.get().getAttachments() != null && !message.get().getAttachments().isEmpty())--%> +<%--
--%> +<%-- @for(var attachment: message.get().getAttachments().stream().filter(AttachmentFormatUtils::isMediaAttachment).toList())--%> +<%--
--%> +<%-- @template.macros.MediaAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), galleryIndex = message.getIndex())--%> +<%--
--%> +<%-- @endfor--%> +<%--
--%> +<%--
--%> +<%-- @for(var attachment: message.get().getAttachments().stream().filter(a -> !AttachmentFormatUtils.isMediaAttachment(a)).toList())--%> +<%-- @template.macros.FileAttachment(attachment = attachment, message = message.get(), nsfw = modmailLog.isNsfw(), numberFormat = nf)--%> +<%-- @endfor--%> +<%--
--%> +<%-- @endif--%>