From 2d30e7fa3b7fc7fd570e6c69ebcc14f8d7e5ca6e Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Sat, 12 Jul 2025 21:59:42 +0200 Subject: [PATCH 1/8] make request in `getImageUrl` nullable --- .../java/net/modfest/platform/service/ImageService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/platform_api/src/main/java/net/modfest/platform/service/ImageService.java b/platform_api/src/main/java/net/modfest/platform/service/ImageService.java index d004a85..dc7ec25 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/ImageService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/ImageService.java @@ -1,5 +1,6 @@ package net.modfest.platform.service; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import net.modfest.platform.configuration.PlatformConfig; import net.modfest.platform.repository.ImageRepository; @@ -25,7 +26,7 @@ public void downloadSubmissionImage(String url, SubmissionRepository.SubmissionI } } - public String getImageUrl(HttpServletRequest request, SubmissionRepository.SubmissionId subKey, SubmissionImageType type) { + public String getImageUrl(@Nullable HttpServletRequest request, SubmissionRepository.SubmissionId subKey, SubmissionImageType type) { var imageInfo = repository.getImageInfo(getImageLocationKey(subKey, type)); if (imageInfo == null) { return null; @@ -33,6 +34,10 @@ public String getImageUrl(HttpServletRequest request, SubmissionRepository.Submi String imageCdn = config.getImageCdnUrl(); if (imageCdn == null) { + if(request == null) { + return null; + } + // Platform will be acting as the cdn var baseUrl = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort(); imageCdn = baseUrl+"/imagecdn/"; From f7d0299be2f3d1725d69e0b9af6f888b45da8250 Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:16:09 +0200 Subject: [PATCH 2/8] Return data on delete and add `size` --- .../platform/repository/AbstractJsonRepository.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platform_api/src/main/java/net/modfest/platform/repository/AbstractJsonRepository.java b/platform_api/src/main/java/net/modfest/platform/repository/AbstractJsonRepository.java index e7fb073..ace6ad3 100644 --- a/platform_api/src/main/java/net/modfest/platform/repository/AbstractJsonRepository.java +++ b/platform_api/src/main/java/net/modfest/platform/repository/AbstractJsonRepository.java @@ -122,7 +122,7 @@ public void save(@NonNull Data newData) { } @Locked.Write("dataLock") - public void delete(@NonNull Id id) { + public Data delete(@NonNull Id id) { var data = this.get(id); this.root.write(p -> { try { @@ -136,6 +136,8 @@ public void delete(@NonNull Id id) { this.store.remove(id); onDataUpdated.emit(id); + + return data; } @Locked.Read("dataLock") @@ -155,6 +157,11 @@ public Collection getAll() { return this.store.values(); } + @Locked.Read("dataLock") + public int size() { + return this.store.size(); + } + private void validateId(@NonNull String id) throws ConstraintViolationException { if (id.contains("/")) { throw new ConstraintViolationException("Illegal character '/' in id: '"+id+"'"); From b2134159c08b417e6316059b6e248914e8aaf94c Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:16:20 +0200 Subject: [PATCH 3/8] Enable Async --- .../src/main/java/net/modfest/platform/PlatformApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platform_api/src/main/java/net/modfest/platform/PlatformApplication.java b/platform_api/src/main/java/net/modfest/platform/PlatformApplication.java index 4179875..8453001 100644 --- a/platform_api/src/main/java/net/modfest/platform/PlatformApplication.java +++ b/platform_api/src/main/java/net/modfest/platform/PlatformApplication.java @@ -3,10 +3,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableAsync @ConfigurationPropertiesScan public class PlatformApplication { From 9f1ce3df635d1873062b57b7b6ded1da6748064f Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:16:34 +0200 Subject: [PATCH 4/8] Add webhook service --- .../platform/service/SubmissionService.java | 83 ++++--- .../platform/service/WebhookService.java | 212 ++++++++++++++++++ .../src/main/resources/application.properties | 2 + 3 files changed, 266 insertions(+), 31 deletions(-) create mode 100644 platform_api/src/main/java/net/modfest/platform/service/WebhookService.java diff --git a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java index 538a811..849cbc1 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java @@ -23,6 +23,8 @@ public class SubmissionService { private SubmissionRepository submissionRepository; @Autowired private UserService userService; + @Autowired + private WebhookService webhookService; @Lazy @Autowired private EventService eventService; @@ -61,6 +63,8 @@ public SubmissionData editSubmission(SubmissionData data, SubmissionPatchData ed throw new IllegalStateException(); } } + + webhookService.editSubmission(edit, data); submissionRepository.save(data); return getSubmission(data.event(), data.id()); } @@ -71,7 +75,8 @@ public SubmissionData editSubmissionBooth(SubmissionData data, SubmissionData.Bo if (edit.warp() == null) edit = edit.withWarp(data.boothData().warp()); if (edit.itemIcon() == null) edit = edit.withItemIcon(data.boothData().itemIcon()); if (edit.shards() == null) edit = edit.withShards(data.boothData().shards()); - if (edit.minutesToComplete() == null) edit = edit.withMinutesToComplete(data.boothData().minutesToComplete()); + if (edit.minutesToComplete() == null) + edit = edit.withMinutesToComplete(data.boothData().minutesToComplete()); if (edit.status() == null) edit = edit.withStatus(data.boothData().status()); } data = data.withBoothData(edit); @@ -90,7 +95,8 @@ public SubmissionData updateSubmissionVersion(SubmissionData data) { throw new IllegalArgumentException("Modrinth project not found!"); } - var latest = getLatestModrinth(mr.projectId(), eventService.getEventById(data.event()), project.projectType); + var versions = getModrinthVersions(mr.projectId(), eventService.getEventById(data.event()), project.projectType); + var latest = pickModrinthLatest(versions); var newData = data.withPlatform(new SubmissionData.AssociatedData( new SubmissionData.AssociatedData.Modrinth( @@ -99,6 +105,7 @@ public SubmissionData updateSubmissionVersion(SubmissionData data) { ) )); + webhookService.updateSubmissionVersion(newData, latest, mr, versions); submissionRepository.save(newData); return getSubmission(newData.event(), newData.id()); } @@ -170,11 +177,13 @@ public SubmissionData addSubmissionAuthor(SubmissionData data, UserData author) } public void deleteSubmission(String eventId, String subId) { - submissionRepository.delete(new SubmissionRepository.SubmissionId(eventId, subId)); + var data = submissionRepository.delete(new SubmissionRepository.SubmissionId(eventId, subId)); + webhookService.deleteSubmission(data); } /** * Retrieve all submissions made by a user + * * @param filter If non-null, only submissions associated with that event will be returned */ public Stream getSubmissionsFromUser(UserData user, @Nullable EventData filter) { @@ -189,8 +198,8 @@ public Stream getSubmissionsFromUser(UserData user, @Nullable Ev public Stream getSubmissionsFromEvent(EventData event) { return submissionRepository.getAll() - .stream() - .filter(s -> s.event().equals(event.id())); + .stream() + .filter(s -> s.event().equals(event.id())); } public Stream getUsersForRinthProject(String modrinthProjectId) { @@ -201,9 +210,9 @@ public Stream getUsersForRinthProject(String modrinthProjectId) { if (members == null) return null; return Stream.concat( - members.stream(), - (organization == null ? List.of() : organization.members).stream() - ).map(mrData -> userService.getByModrinthId(mrData.user.id)) + members.stream(), + (organization == null ? List.of() : organization.members).stream() + ).map(mrData -> userService.getByModrinthId(mrData.user.id)) .filter(Objects::nonNull); } @@ -240,6 +249,7 @@ public SubmissionData makeSubmissionOther(EventData event, Set authors ) ); submissionRepository.save(submission); + webhookService.makeSubmission(submission, submissionRepository.size()); return submission; } @@ -253,7 +263,8 @@ public SubmissionData makeSubmissionModrinth(String eventId, Set autho throw new RuntimeException("submission already exists"); } - var latest = getLatestModrinth(subId, eventService.getEventById(eventId), project.projectType); + var versions = getModrinthVersions(subId, eventService.getEventById(eventId), project.projectType); + var latest = pickModrinthLatest(versions); if (project.iconUrl != null) { imageService.downloadSubmissionImage(project.iconUrl, subKey, ImageService.SubmissionImageType.ICON); @@ -262,27 +273,29 @@ public SubmissionData makeSubmissionModrinth(String eventId, Set autho if (galleryUrl != null) { imageService.downloadSubmissionImage(galleryUrl, subKey, ImageService.SubmissionImageType.SCREENSHOT); } - submissionRepository.save( - new SubmissionData( - subId, - eventId, - project.title, - project.description, - authors.stream().map(UserData::id).collect(Collectors.toSet()), - new SubmissionData.AssociatedData( - new SubmissionData.AssociatedData.Modrinth( - project.id, - latest == null ? null : latest.id - ) - ), - project.sourceUrl, - null, - new SubmissionData.Awards( - Set.of(), - Set.of() + var submission = new SubmissionData( + subId, + eventId, + project.title, + project.description, + authors.stream().map(UserData::id).collect(Collectors.toSet()), + new SubmissionData.AssociatedData( + new SubmissionData.AssociatedData.Modrinth( + project.id, + latest == null ? null : latest.id ) + ), + project.sourceUrl, + null, + new SubmissionData.Awards( + Set.of(), + Set.of() ) ); + + submissionRepository.save(submission); + + webhookService.makeSubmission(submission, submissionRepository.size()); return submissionRepository.get(subKey); } @@ -296,17 +309,25 @@ public SubmissionData makeSubmissionModrinth(String eventId, Set autho } /** - * Retrieves the most recent version of a modrinth project + * Retrieves fitting versions of a modrinth project * * @param event The event this version will be for. Used for filtering * @param projectType The modrinth project type. Used for filtering */ - private @Nullable Version getLatestModrinth(String mrProject, EventData event, String projectType) { + private @Nullable List getModrinthVersions(String mrProject, EventData event, String projectType) { if (projectType.equals("modpack")) return null; var filter = VersionFilter.ofLoaders(List.of(event.mod_loader(), "minecraft", "datapack", "iris")) .andGameVersion(event.minecraft_version()); - return modrinth.projects().getVersions(mrProject, filter) - .stream() + + return modrinth.projects().getVersions(mrProject, filter); + } + + private @Nullable Version pickModrinthLatest(@Nullable List versions) { + if (versions == null) { + return null; + } + + return versions.stream() .max(Comparator.comparing(v -> v.datePublished)) .orElse(null); } diff --git a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java new file mode 100644 index 0000000..9987058 --- /dev/null +++ b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java @@ -0,0 +1,212 @@ +package net.modfest.platform.service; + +import jakarta.annotation.Nullable; +import net.modfest.platform.pojo.SubmissionData; +import net.modfest.platform.pojo.SubmissionPatchData; +import net.modfest.platform.repository.SubmissionRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import nl.theepicblock.dukerinth.models.Version; + +import java.util.*; + +@Service +public class WebhookService { + private static final Logger LOGGER = LoggerFactory.getLogger(WebhookService.class); + + @Value("${webhook.discord.url}") + private String discordWebhookUrl; + + @Autowired + private ImageService imageService; + + private final RestTemplate restTemplate = new RestTemplate(); + + protected record Field(String name, String value) { + } + + protected record Footer(String text, @Nullable String icon_url) { + } + + protected record Image(String url) { + } + + protected record Embed(String title, String description, int color, List fields, Footer footer, + Image thumbnail) { + } + + protected record WebhookPayload(List embeds) { + } + + Embed embedForSubmission(SubmissionData data, String event, int eventColor) { + return embedForSubmission(data, event, eventColor, null, null); + } + + Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields) { + return embedForSubmission(data, event, eventColor, fields, null); + } + + Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields, @Nullable Footer footer) { + return new Embed( + data.name(), + event, + eventColor, + fields, + footer, + getIcon(data) + ); + } + + private @Nullable Image getIcon(SubmissionData data) { + var subKey = new SubmissionRepository.SubmissionId(data.event(), data.id()); + var icon = imageService.getImageUrl(null, subKey, ImageService.SubmissionImageType.ICON); + + if (icon == null) { + return null; + } + + return new Image(icon); + } + + protected void sendEmbed(Embed embed) { + if (discordWebhookUrl == null) { + return; + } + + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + var payload = new WebhookPayload(List.of(embed)); + + try { + ResponseEntity response = restTemplate.postForEntity(discordWebhookUrl, new HttpEntity<>(payload, headers), String.class); + if (!response.getStatusCode().is2xxSuccessful()) { + LOGGER.error("Failed to send Discord embed: {}", response.getStatusCode()); + } + } catch (Exception e) { + LOGGER.error("Error sending Discord webhook: {}", e.getMessage()); + } + } + + + private String maskedLink(String name, String url) { + return String.format("[%s](%s)", name, url); + } + + private String projectUrl(String projectId) { + return "https://modrinth.com/project/" + projectId; + } + + private String formatVersion(Version version) { + return maskedLink(version.versionNumber, projectUrl(version.projectId) + "/version/" + version.id); + } + + @Async + public void makeSubmission(SubmissionData data, int submissionCount) { + var fields = new ArrayList(); + + fields.add(new Field("Source", data.source())); + + if ((data.platform().inner() instanceof SubmissionData.AssociatedData.Modrinth mr)) { + fields.add(new Field("Project", projectUrl(mr.projectId()))); + } + + var embed = embedForSubmission( + data, + "Submitted", + 3466376, + fields, + new Footer(submissionCount + " submissions so far", null) + ); + + sendEmbed(embed); + } + + @Async + public void updateSubmissionVersion(SubmissionData data, @Nullable Version newVersion, SubmissionData.AssociatedData.Modrinth mr, @Nullable List versions) { + var fields = new ArrayList(); + + if (newVersion != null) { + fields.add(new Field("Version", formatVersion(newVersion))); + fields.add(new Field("Changelog", newVersion.changelog)); + } + + if (versions != null) { + var previousVersion = versions.stream().filter(v -> v.id.equals(mr.versionId())).findAny(); + previousVersion.ifPresent(version -> fields.add(new Field("Previous Version", formatVersion(version)))); + } + + var embed = embedForSubmission( + data, + "Updated Version", + 5814783, + fields + ); + + sendEmbed(embed); + } + + @Async + public void editSubmission(SubmissionPatchData edit, SubmissionData data) { + var fields = new ArrayList(); + + if (edit.sourceUrl() != null) { + fields.add(new Field("Source url", edit.sourceUrl())); + } + + if (edit.downloadUrl() != null) { + fields.add(new Field("Download url", edit.downloadUrl())); + } + + if (edit.homepage() != null) { + fields.add(new Field("Homepage", edit.homepage())); + } + + if (fields.isEmpty()) { + return; + } + + var embed = embedForSubmission( + data, + "Edited submission", + 5814783, + fields + ); + + sendEmbed(embed); + } + + @Async + public void deleteSubmission(SubmissionData data) { + var embed = embedForSubmission( + data, + "Submission revoked", + 16715365 + ); + + sendEmbed(embed); + } + + @Async + public void phaseChanged(String eventName, String phase, int count) { + var embed = new Embed( + eventName, + String.format("Phase switched to \"%s\"", phase), + 16772110, + null, + new Footer(String.format("Currently %d submissions", count), null), + null + ); + + sendEmbed(embed); + } +} diff --git a/platform_api/src/main/resources/application.properties b/platform_api/src/main/resources/application.properties index cc3d7d5..0951cb3 100644 --- a/platform_api/src/main/resources/application.properties +++ b/platform_api/src/main/resources/application.properties @@ -14,6 +14,8 @@ platform.logdir=/var/lib/platform/logs git.user=Platform git.email=platform_automation@modfest.net +webhook.discord.url=\${DISCORD_WEBHOOK_URL} + # These values need to be correct in order for platform to work: spring.http.converters.preferred-json-mapper=gson spring.mvc.converters.preferred-json-mapper=gson From 182ce8086627ecf6e965b74ddefe4d8a857491d8 Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:17:30 +0200 Subject: [PATCH 5/8] Add booth and submission image embeds --- .../modfest/platform/pojo/SubmissionData.java | 16 +++- .../platform/controller/EventController.java | 6 +- .../platform/service/SubmissionService.java | 3 + .../platform/service/WebhookService.java | 85 ++++++++++++++++--- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/net/modfest/platform/pojo/SubmissionData.java b/common/src/main/java/net/modfest/platform/pojo/SubmissionData.java index 8f4e9f4..d08f3cd 100644 --- a/common/src/main/java/net/modfest/platform/pojo/SubmissionData.java +++ b/common/src/main/java/net/modfest/platform/pojo/SubmissionData.java @@ -19,7 +19,7 @@ public record SubmissionData(@NonNull String id, @Nullable String source, @Nullable BoothData boothData, @NonNull Awards awards - ) implements Data { +) implements Data { public record Awards(Set theme, Set extra) { } @@ -68,8 +68,8 @@ public JsonElement serialize(AssociatedData src, var jsonObj = context.serialize(src.inner).getAsJsonObject(); var typeKey = switch (src.inner) { - case Modrinth a -> Modrinth.KEY; - case Other a -> Other.KEY; + case Modrinth ignored -> Modrinth.KEY; + case Other ignored -> Other.KEY; default -> throw new IllegalStateException(); }; jsonObj.addProperty("type", typeKey); @@ -93,7 +93,11 @@ public enum BoothStatus { COMPLETE } - public record Column(int x, int z) {} + public record Column(int x, int z) { + public String toFormattedString() { + return "x: " + x + ", z: " + z; + } + } public record WarpCoordinates(int x, int y, int z, @NonNull Direction direction) { @Getter @@ -121,6 +125,10 @@ public enum Direction { this.yaw = yaw; } } + + public String toFormattedString() { + return "x: " + x + ", z: " + z + ", direction: " + direction.name(); + } } } } diff --git a/platform_api/src/main/java/net/modfest/platform/controller/EventController.java b/platform_api/src/main/java/net/modfest/platform/controller/EventController.java index b3998d1..8e60668 100644 --- a/platform_api/src/main/java/net/modfest/platform/controller/EventController.java +++ b/platform_api/src/main/java/net/modfest/platform/controller/EventController.java @@ -9,6 +9,7 @@ import net.modfest.platform.service.EventService; import net.modfest.platform.service.ImageService; import net.modfest.platform.service.SubmissionService; +import net.modfest.platform.service.WebhookService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +34,8 @@ public class EventController { private UserController userController; @Autowired private ImageService imageService; + @Autowired + private WebhookService webhookService; @GetMapping("/events") public Collection getAllEvents() { @@ -295,9 +298,10 @@ public SubmissionResponseData editSubmissionImage(HttpServletRequest request, @P }; imageService.downloadSubmissionImage(url, new SubmissionRepository.SubmissionId(eventId, subId), typeEnum); + webhookService.submissionImageChanged(submission, typeEnum); return service.addResponseInfo( - request, service.getSubmission(eventId, subId) + request, submission ); } diff --git a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java index 849cbc1..6fc64e7 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java @@ -81,6 +81,9 @@ public SubmissionData editSubmissionBooth(SubmissionData data, SubmissionData.Bo } data = data.withBoothData(edit); submissionRepository.save(data); + + webhookService.editSubmissionBooth(data, edit); + return getSubmission(data.event(), data.id()); } diff --git a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java index 9987058..848e078 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java @@ -31,44 +31,49 @@ public class WebhookService { private final RestTemplate restTemplate = new RestTemplate(); - protected record Field(String name, String value) { + private record Field(String name, String value) { } - protected record Footer(String text, @Nullable String icon_url) { + private record Footer(String text, @Nullable String icon_url) { } - protected record Image(String url) { + private record Image(String url) { } - protected record Embed(String title, String description, int color, List fields, Footer footer, + private record Embed(String title, String description, int color, List fields, Footer footer, Image image, Image thumbnail) { } - protected record WebhookPayload(List embeds) { + private record WebhookPayload(List embeds) { } - Embed embedForSubmission(SubmissionData data, String event, int eventColor) { + private Embed embedForSubmission(SubmissionData data, String event, int eventColor) { return embedForSubmission(data, event, eventColor, null, null); } - Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields) { + private Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields) { return embedForSubmission(data, event, eventColor, fields, null); } - Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields, @Nullable Footer footer) { + private Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields, @Nullable Footer footer) { + return embedForSubmission(data, event, eventColor, fields, footer, null); + } + + private Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nullable List fields, @Nullable Footer footer, @Nullable Image image) { return new Embed( data.name(), event, eventColor, fields, footer, - getIcon(data) + image, + getImage(data, ImageService.SubmissionImageType.ICON) ); } - private @Nullable Image getIcon(SubmissionData data) { + private @Nullable Image getImage(SubmissionData data, ImageService.SubmissionImageType type) { var subKey = new SubmissionRepository.SubmissionId(data.event(), data.id()); - var icon = imageService.getImageUrl(null, subKey, ImageService.SubmissionImageType.ICON); + var icon = imageService.getImageUrl(null, subKey, type); if (icon == null) { return null; @@ -77,7 +82,7 @@ Embed embedForSubmission(SubmissionData data, String event, int eventColor, @Nul return new Image(icon); } - protected void sendEmbed(Embed embed) { + private void sendEmbed(Embed embed) { if (discordWebhookUrl == null) { return; } @@ -97,6 +102,12 @@ protected void sendEmbed(Embed embed) { } } + private String timestampRelative(int minutes) { + var currentTime = System.currentTimeMillis() / 1000L; + var time = currentTime + minutes * 60L; + + return ""; + } private String maskedLink(String name, String url) { return String.format("[%s](%s)", name, url); @@ -196,6 +207,55 @@ public void deleteSubmission(SubmissionData data) { sendEmbed(embed); } + @Async + public void submissionImageChanged(SubmissionData data, ImageService.SubmissionImageType type) { + var typeString = switch (type) { + case TEST -> "Test"; + case CLAIM -> "Claim"; + case BUILD -> "Build"; + default -> null; + }; + + if (typeString == null) { + return; + } + + var embed = embedForSubmission(data, typeString + " image set", 16777048, null, null, getImage(data, type)); + sendEmbed(embed); + } + + @Async + public void editSubmissionBooth(SubmissionData data, SubmissionData.BoothData edit) { + var fields = new ArrayList(); + + if (edit.itemIcon() != null) { + fields.add(new Field("Item icon", edit.itemIcon())); + } + + if (edit.markerPos() != null) { + fields.add(new Field("Marker position", edit.markerPos().toFormattedString())); + } + + if (edit.minutesToComplete() != null) { + fields.add(new Field("Complete", timestampRelative(edit.minutesToComplete()))); + } + + if(edit.shards() != null) { + fields.add(new Field("Shards", edit.shards().toString())); + } + + if(edit.status() != null) { + fields.add(new Field("Status", edit.status().name())); + } + + if(edit.warp() != null) { + fields.add(new Field("Warp", edit.warp().toFormattedString())); + } + + var embed = embedForSubmission(data, "Boot data changed", 16777048, fields); + sendEmbed(embed); + } + @Async public void phaseChanged(String eventName, String phase, int count) { var embed = new Embed( @@ -204,6 +264,7 @@ public void phaseChanged(String eventName, String phase, int count) { 16772110, null, new Footer(String.format("Currently %d submissions", count), null), + null, null ); From cabbf9a062dde819e2ae74527b39bd2466ca0ac9 Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:38:33 +0200 Subject: [PATCH 6/8] store webhook url on event data instead of using environment variable --- .../net/modfest/platform/pojo/EventData.java | 5 ++- .../platform/controller/EventController.java | 2 +- .../platform/service/SubmissionService.java | 20 +++++---- .../platform/service/WebhookService.java | 43 +++++++++---------- .../src/main/resources/application.properties | 2 - .../java/net/modfest/platform/JsonTests.java | 3 +- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/common/src/main/java/net/modfest/platform/pojo/EventData.java b/common/src/main/java/net/modfest/platform/pojo/EventData.java index fd101c5..a6c0e2e 100644 --- a/common/src/main/java/net/modfest/platform/pojo/EventData.java +++ b/common/src/main/java/net/modfest/platform/pojo/EventData.java @@ -2,6 +2,7 @@ import lombok.With; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.time.Instant; import java.util.List; @@ -17,7 +18,9 @@ public record EventData(@NonNull String id, @NonNull DiscordRoles discordRoles, @NonNull String mod_loader, @NonNull String minecraft_version, - @NonNull String modpack) implements Data { + @NonNull String modpack, + @Nullable String discordWebhookUrl +) implements Data { public enum Type { MODFEST, BLANKETCON diff --git a/platform_api/src/main/java/net/modfest/platform/controller/EventController.java b/platform_api/src/main/java/net/modfest/platform/controller/EventController.java index d17c84f..25e66d6 100644 --- a/platform_api/src/main/java/net/modfest/platform/controller/EventController.java +++ b/platform_api/src/main/java/net/modfest/platform/controller/EventController.java @@ -300,7 +300,7 @@ public SubmissionResponseData editSubmissionImage(HttpServletRequest request, @P }; imageService.downloadSubmissionImage(url, new SubmissionRepository.SubmissionId(eventId, subId), typeEnum); - webhookService.submissionImageChanged(submission, typeEnum); + webhookService.submissionImageChanged(event, submission, typeEnum); return service.addResponseInfo( request, submission diff --git a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java index 67885da..c4abc04 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java @@ -66,7 +66,7 @@ public SubmissionData editSubmission(SubmissionData data, SubmissionPatchData ed } } - webhookService.editSubmission(edit, data); + webhookService.editSubmission(eventService.getEventById(data.event()), edit, data); submissionRepository.save(data); return getSubmission(data.event(), data.id()); } @@ -84,7 +84,7 @@ public SubmissionData editSubmissionBooth(SubmissionData data, SubmissionData.Bo data = data.withBoothData(edit); submissionRepository.save(data); - webhookService.editSubmissionBooth(data, edit); + webhookService.editSubmissionBooth(eventService.getEventById(data.event()), data, edit); return getSubmission(data.event(), data.id()); } @@ -100,7 +100,9 @@ public SubmissionData updateSubmissionVersion(SubmissionData data) { throw new IllegalArgumentException("Modrinth project not found!"); } - var versions = getModrinthVersions(mr.projectId(), eventService.getEventById(data.event()), project.projectType); + var event = eventService.getEventById(data.event()); + + var versions = getModrinthVersions(mr.projectId(), event, project.projectType); var latest = pickModrinthLatest(versions); var newData = data.withPlatform(new SubmissionData.AssociatedData( @@ -110,7 +112,7 @@ public SubmissionData updateSubmissionVersion(SubmissionData data) { ) )); - webhookService.updateSubmissionVersion(newData, latest, mr, versions); + webhookService.updateSubmissionVersion(event, newData, latest, mr, versions); submissionRepository.save(newData); return getSubmission(newData.event(), newData.id()); } @@ -183,7 +185,7 @@ public SubmissionData addSubmissionAuthor(SubmissionData data, UserData author) public void deleteSubmission(String eventId, String subId) { var data = submissionRepository.delete(new SubmissionRepository.SubmissionId(eventId, subId)); - webhookService.deleteSubmission(data); + webhookService.deleteSubmission(eventService.getEventById(eventId), data); } /** @@ -255,7 +257,7 @@ public SubmissionData makeSubmissionOther(EventData event, Set authors ) ); submissionRepository.save(submission); - webhookService.makeSubmission(submission, submissionRepository.size()); + webhookService.makeSubmission(event, submission, submissionRepository.size()); return submission; } @@ -269,7 +271,9 @@ public SubmissionData makeSubmissionModrinth(String eventId, Set autho throw new RuntimeException("submission already exists"); } - var versions = getModrinthVersions(subId, eventService.getEventById(eventId), project.projectType); + var event = eventService.getEventById(eventId); + + var versions = getModrinthVersions(subId, event, project.projectType); var latest = pickModrinthLatest(versions); if (project.sourceUrl == null || project.sourceUrl.isBlank()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, @@ -306,7 +310,7 @@ public SubmissionData makeSubmissionModrinth(String eventId, Set autho ); submissionRepository.save(submission); - webhookService.makeSubmission(submission, submissionRepository.size()); + webhookService.makeSubmission(event, submission, submissionRepository.size()); return submissionRepository.get(subKey); } diff --git a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java index 848e078..6ad28d5 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java @@ -1,13 +1,13 @@ package net.modfest.platform.service; -import jakarta.annotation.Nullable; +import net.modfest.platform.pojo.EventData; import net.modfest.platform.pojo.SubmissionData; import net.modfest.platform.pojo.SubmissionPatchData; import net.modfest.platform.repository.SubmissionRepository; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -23,9 +23,6 @@ public class WebhookService { private static final Logger LOGGER = LoggerFactory.getLogger(WebhookService.class); - @Value("${webhook.discord.url}") - private String discordWebhookUrl; - @Autowired private ImageService imageService; @@ -82,8 +79,8 @@ private Embed embedForSubmission(SubmissionData data, String event, int eventCol return new Image(icon); } - private void sendEmbed(Embed embed) { - if (discordWebhookUrl == null) { + private void sendEmbed(EventData event, Embed embed) { + if (event.discordWebhookUrl() == null) { return; } @@ -93,7 +90,7 @@ private void sendEmbed(Embed embed) { var payload = new WebhookPayload(List.of(embed)); try { - ResponseEntity response = restTemplate.postForEntity(discordWebhookUrl, new HttpEntity<>(payload, headers), String.class); + ResponseEntity response = restTemplate.postForEntity(event.discordWebhookUrl(), new HttpEntity<>(payload, headers), String.class); if (!response.getStatusCode().is2xxSuccessful()) { LOGGER.error("Failed to send Discord embed: {}", response.getStatusCode()); } @@ -122,7 +119,7 @@ private String formatVersion(Version version) { } @Async - public void makeSubmission(SubmissionData data, int submissionCount) { + public void makeSubmission(EventData event, SubmissionData data, int submissionCount) { var fields = new ArrayList(); fields.add(new Field("Source", data.source())); @@ -139,11 +136,11 @@ public void makeSubmission(SubmissionData data, int submissionCount) { new Footer(submissionCount + " submissions so far", null) ); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void updateSubmissionVersion(SubmissionData data, @Nullable Version newVersion, SubmissionData.AssociatedData.Modrinth mr, @Nullable List versions) { + public void updateSubmissionVersion(EventData event, SubmissionData data, @Nullable Version newVersion, SubmissionData.AssociatedData.Modrinth mr, @Nullable List versions) { var fields = new ArrayList(); if (newVersion != null) { @@ -163,11 +160,11 @@ public void updateSubmissionVersion(SubmissionData data, @Nullable Version newVe fields ); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void editSubmission(SubmissionPatchData edit, SubmissionData data) { + public void editSubmission(EventData event, SubmissionPatchData edit, SubmissionData data) { var fields = new ArrayList(); if (edit.sourceUrl() != null) { @@ -193,22 +190,22 @@ public void editSubmission(SubmissionPatchData edit, SubmissionData data) { fields ); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void deleteSubmission(SubmissionData data) { + public void deleteSubmission(EventData event, SubmissionData data) { var embed = embedForSubmission( data, "Submission revoked", 16715365 ); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void submissionImageChanged(SubmissionData data, ImageService.SubmissionImageType type) { + public void submissionImageChanged(EventData event, SubmissionData data, ImageService.SubmissionImageType type) { var typeString = switch (type) { case TEST -> "Test"; case CLAIM -> "Claim"; @@ -221,11 +218,11 @@ public void submissionImageChanged(SubmissionData data, ImageService.SubmissionI } var embed = embedForSubmission(data, typeString + " image set", 16777048, null, null, getImage(data, type)); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void editSubmissionBooth(SubmissionData data, SubmissionData.BoothData edit) { + public void editSubmissionBooth(EventData event, SubmissionData data, SubmissionData.BoothData edit) { var fields = new ArrayList(); if (edit.itemIcon() != null) { @@ -253,13 +250,13 @@ public void editSubmissionBooth(SubmissionData data, SubmissionData.BoothData ed } var embed = embedForSubmission(data, "Boot data changed", 16777048, fields); - sendEmbed(embed); + sendEmbed(event, embed); } @Async - public void phaseChanged(String eventName, String phase, int count) { + public void phaseChanged(EventData event, String phase, int count) { var embed = new Embed( - eventName, + event.name(), String.format("Phase switched to \"%s\"", phase), 16772110, null, @@ -268,6 +265,6 @@ public void phaseChanged(String eventName, String phase, int count) { null ); - sendEmbed(embed); + sendEmbed(event, embed); } } diff --git a/platform_api/src/main/resources/application.properties b/platform_api/src/main/resources/application.properties index 0951cb3..cc3d7d5 100644 --- a/platform_api/src/main/resources/application.properties +++ b/platform_api/src/main/resources/application.properties @@ -14,8 +14,6 @@ platform.logdir=/var/lib/platform/logs git.user=Platform git.email=platform_automation@modfest.net -webhook.discord.url=\${DISCORD_WEBHOOK_URL} - # These values need to be correct in order for platform to work: spring.http.converters.preferred-json-mapper=gson spring.mvc.converters.preferred-json-mapper=gson diff --git a/platform_api/src/test/java/net/modfest/platform/JsonTests.java b/platform_api/src/test/java/net/modfest/platform/JsonTests.java index 53396cf..693bd88 100644 --- a/platform_api/src/test/java/net/modfest/platform/JsonTests.java +++ b/platform_api/src/test/java/net/modfest/platform/JsonTests.java @@ -63,7 +63,8 @@ public static List testObjects() { ), "optifine", "1.34", - "boop" + "boop", + "https://discord.com/api/webhooks/123/123" ), new SubmissionData( "a", From a785844301b92ab81b236b339bc0fa3f93051e65 Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:05:50 +0200 Subject: [PATCH 7/8] only show update submission version embed if the version actually changed --- .../java/net/modfest/platform/service/SubmissionService.java | 4 +++- .../java/net/modfest/platform/service/WebhookService.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java index c4abc04..4a7d380 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/SubmissionService.java @@ -112,7 +112,9 @@ public SubmissionData updateSubmissionVersion(SubmissionData data) { ) )); - webhookService.updateSubmissionVersion(event, newData, latest, mr, versions); + if(latest != null && !Objects.equals(mr.versionId(), latest.id)) { + webhookService.updateSubmissionVersion(event, newData, latest, mr, versions); + } submissionRepository.save(newData); return getSubmission(newData.event(), newData.id()); } diff --git a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java index 6ad28d5..e097f46 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java @@ -140,7 +140,7 @@ public void makeSubmission(EventData event, SubmissionData data, int submissionC } @Async - public void updateSubmissionVersion(EventData event, SubmissionData data, @Nullable Version newVersion, SubmissionData.AssociatedData.Modrinth mr, @Nullable List versions) { + public void updateSubmissionVersion(EventData event, SubmissionData data, @Nullable Version newVersion, SubmissionData.AssociatedData.Modrinth mrPrevious, @Nullable List versions) { var fields = new ArrayList(); if (newVersion != null) { @@ -149,7 +149,7 @@ public void updateSubmissionVersion(EventData event, SubmissionData data, @Nulla } if (versions != null) { - var previousVersion = versions.stream().filter(v -> v.id.equals(mr.versionId())).findAny(); + var previousVersion = versions.stream().filter(v -> v.id.equals(mrPrevious.versionId())).findAny(); previousVersion.ifPresent(version -> fields.add(new Field("Previous Version", formatVersion(version)))); } From 28ac254b9f31b0f6c2716507c158e0027ce475ee Mon Sep 17 00:00:00 2001 From: Tomate0613 <69756782+Tomate0613@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:10:40 +0200 Subject: [PATCH 8/8] rename variable --- .../java/net/modfest/platform/service/WebhookService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java index e097f46..cf03c03 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/WebhookService.java @@ -70,13 +70,13 @@ private Embed embedForSubmission(SubmissionData data, String event, int eventCol private @Nullable Image getImage(SubmissionData data, ImageService.SubmissionImageType type) { var subKey = new SubmissionRepository.SubmissionId(data.event(), data.id()); - var icon = imageService.getImageUrl(null, subKey, type); + var url = imageService.getImageUrl(null, subKey, type); - if (icon == null) { + if (url == null) { return null; } - return new Image(icon); + return new Image(url); } private void sendEmbed(EventData event, Embed embed) {