Skip to content

Commit 4cdff58

Browse files
feat: Add /image upload command for showcase server.
1 parent b2b53b1 commit 4cdff58

File tree

3 files changed

+231
-0
lines changed

3 files changed

+231
-0
lines changed

src/main/java/net/modgarden/gardenbot/GardenBotCommands.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.dv8tion.jda.api.interactions.commands.OptionType;
44
import net.modgarden.gardenbot.commands.account.*;
55
import net.modgarden.gardenbot.commands.event.*;
6+
import net.modgarden.gardenbot.commands.image.UploadHandler;
67
import net.modgarden.gardenbot.interaction.command.SlashCommand;
78
import net.modgarden.gardenbot.interaction.command.SlashCommandDispatcher;
89
import net.modgarden.gardenbot.interaction.command.SlashCommandOption;
@@ -111,6 +112,15 @@ public static void registerAll() {
111112
new SlashCommandOption(OptionType.STRING, "version", "The version of the project to update to.", false, true)
112113
)));
113114

115+
SlashCommandDispatcher.register(new SlashCommand("image", "Actions relating to images for Mod Garden's showcase worlds.",
116+
new SlashCommand.SubCommand(
117+
"upload",
118+
"Uploads an image to the public Mod Garden CDN.",
119+
UploadHandler::handleUpload,
120+
new SlashCommandOption(OptionType.ATTACHMENT, "attachment", "A PNG image to upload.", true)
121+
)
122+
));
123+
114124
// TODO: Implement Ban command.
115125
// SlashCommandDispatcher.register(new SlashCommand("ban", "Bans a user from the Mod Garden Discord.", BanCommandHandler::handleBan,
116126
// new SlashCommandOption(OptionType.USER, "user", "The user to ban.", true),
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package net.modgarden.gardenbot.commands.image;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonParser;
5+
import com.google.gson.annotations.SerializedName;
6+
import net.dv8tion.jda.api.entities.Message;
7+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
8+
import net.modgarden.gardenbot.GardenBot;
9+
import net.modgarden.gardenbot.interaction.SlashCommandInteraction;
10+
import net.modgarden.gardenbot.interaction.response.EmbedResponse;
11+
import net.modgarden.gardenbot.interaction.response.Response;
12+
import net.modgarden.gardenbot.util.BunnyCDNAPIClient;
13+
import net.modgarden.gardenbot.util.ModGardenAPIClient;
14+
15+
import java.io.IOException;
16+
import java.io.InputStream;
17+
import java.io.InputStreamReader;
18+
import java.net.http.HttpRequest;
19+
import java.net.http.HttpResponse;
20+
import java.util.List;
21+
import java.util.Random;
22+
23+
public class UploadHandler {
24+
public static Response handleUpload(SlashCommandInteraction interaction) {
25+
interaction.event().deferReply(false).queue();
26+
27+
ModGardenEvent event;
28+
Message.Attachment attachment = interaction.event().getOption("attachment", OptionMapping::getAsAttachment);
29+
30+
if (attachment == null || !attachment.isImage() || attachment.getContentType() == null || !attachment.getContentType().equals("image/png")) {
31+
return new EmbedResponse()
32+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
33+
.setDescription("Attachment must be a PNG.")
34+
.setColor(0x5D3E40);
35+
}
36+
37+
try {
38+
var eventResult = ModGardenAPIClient.get("events/current/prefreeze", HttpResponse.BodyHandlers.ofInputStream());
39+
if (eventResult.statusCode() != 200) {
40+
return new EmbedResponse()
41+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
42+
.setDescription("There is no active event to upload images for.")
43+
.setColor(0x5D3E40);
44+
}
45+
try (InputStreamReader eventReader = new InputStreamReader(eventResult.body())) {
46+
event = GardenBot.GSON.fromJson(eventReader, ModGardenEvent.class);
47+
}
48+
} catch (IOException | InterruptedException ex) {
49+
GardenBot.LOG.error("", ex);
50+
return new EmbedResponse()
51+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
52+
.setDescription(ex.getMessage() + "\nPlease report this to a team member.")
53+
.setColor(0xFF0000);
54+
}
55+
56+
ModGardenUser user;
57+
try {
58+
var userResult = ModGardenAPIClient.get("user/" + interaction.event().getUser().getId() + "?service=discord", HttpResponse.BodyHandlers.ofInputStream());
59+
if (userResult.statusCode() != 200) {
60+
return new EmbedResponse()
61+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
62+
.setDescription("You do not have a Mod Garden account. Please create one with **/register**.")
63+
.setColor(0x5D3E40);
64+
}
65+
66+
// Validate that the user has submitted to the specified event.
67+
try (InputStreamReader userReader = new InputStreamReader(userResult.body())) {
68+
user = GardenBot.GSON.fromJson(userReader, ModGardenUser.class);
69+
if (!user.events.contains(event.id)) {
70+
return new EmbedResponse()
71+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
72+
.setDescription("You are not associated with any projects within " + event.displayName + ".")
73+
.setColor(0x5D3E40);
74+
}
75+
}
76+
} catch (IOException | InterruptedException ex) {
77+
GardenBot.LOG.error("", ex);
78+
return new EmbedResponse()
79+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
80+
.setDescription(ex.getMessage() + "\nPlease report this to a team member.")
81+
.setColor(0xFF0000);
82+
}
83+
StringBuilder fileNameBuilder = new StringBuilder()
84+
.append(event.slug)
85+
.append("/")
86+
.append(user.id);
87+
88+
String hash;
89+
try {
90+
hash = selectUniqueHash(fileNameBuilder.toString());
91+
} catch (IOException | InterruptedException ex) {
92+
GardenBot.LOG.error("", ex);
93+
return new EmbedResponse()
94+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
95+
.setDescription(ex.getMessage() + "\nPlease report this to a team member.")
96+
.setColor(0xFF0000);
97+
}
98+
if (hash.isBlank()) {
99+
return new EmbedResponse()
100+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
101+
.setDescription("Failed to generate a unique upload hash. Please try again.")
102+
.setColor(0x5D3E40);
103+
}
104+
105+
fileNameBuilder
106+
.append("/")
107+
.append(hash)
108+
.append(".png");
109+
110+
try (InputStream attachmentStream = attachment.getProxy().download(attachment.getWidth(), attachment.getHeight()).join()) {
111+
HttpResponse<InputStream> uploadResponse = BunnyCDNAPIClient.put(
112+
"public/" + fileNameBuilder,
113+
HttpRequest.BodyPublishers.ofInputStream(() -> attachmentStream),
114+
HttpResponse.BodyHandlers.ofInputStream(),
115+
"Content-Type", "application/octet-stream",
116+
"Accept", "image/png"
117+
);
118+
try (InputStreamReader streamReader = new InputStreamReader(uploadResponse.body())) {
119+
if (uploadResponse.statusCode() != 201) {
120+
JsonElement json = JsonParser.parseReader(streamReader);
121+
String errorMessage = json.isJsonObject() && json.getAsJsonObject().has("Message") ?
122+
json.getAsJsonObject().getAsJsonPrimitive("Message").getAsString() :
123+
"Undefined Error.";
124+
return new EmbedResponse()
125+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
126+
.setDescription(uploadResponse.statusCode() + ": " + errorMessage + "\nPlease report this to a team member.")
127+
.setColor(0xFF0000);
128+
}
129+
}
130+
} catch (IOException | InterruptedException ex) {
131+
GardenBot.LOG.error("", ex);
132+
return new EmbedResponse()
133+
.setTitle("Encountered an exception whilst attempting to upload an image to Mod Garden's CDN.")
134+
.setDescription(ex.getMessage() + "\nPlease report this to a team member.")
135+
.setColor(0xFF0000);
136+
}
137+
138+
return new EmbedResponse()
139+
.setTitle("Successfully uploaded image to Mod Garden's CDN")
140+
.setDescription("Your image may be found at <https://cdn.modgarden.net/public/" + fileNameBuilder + ">")
141+
.setColor(0xA9FFA7);
142+
}
143+
144+
public static String selectUniqueHash(String basePath) throws IOException, InterruptedException {
145+
for (int i = 0; i < 20; ++i) {
146+
String hash = generateHash();
147+
HttpResponse<String> getResponse = BunnyCDNAPIClient.get(
148+
basePath + "/" + hash + ".png",
149+
HttpResponse.BodyHandlers.ofString()
150+
);
151+
if (getResponse.statusCode() != 200)
152+
return hash;
153+
}
154+
return "";
155+
}
156+
157+
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
158+
private static class ModGardenUser {
159+
String id;
160+
List<String> events;
161+
}
162+
163+
private static class ModGardenEvent {
164+
String id;
165+
String slug;
166+
@SerializedName("display_name")
167+
String displayName;
168+
}
169+
170+
private static final String VALID_UNIQUE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
171+
private static final Random RANDOM = new Random();
172+
173+
private static String generateHash() {
174+
StringBuilder builder = new StringBuilder();
175+
for (int i = 0; i < 6; ++i) {
176+
builder.append(VALID_UNIQUE_CHARS.charAt(RANDOM.nextInt(VALID_UNIQUE_CHARS.length())));
177+
}
178+
return builder.toString();
179+
}
180+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package net.modgarden.gardenbot.util;
2+
3+
import net.modgarden.gardenbot.GardenBot;
4+
5+
import java.io.IOException;
6+
import java.net.URI;
7+
import java.net.http.HttpRequest;
8+
import java.net.http.HttpResponse;
9+
10+
public class BunnyCDNAPIClient {
11+
public static final String API_URL = "https://ny.storage.bunnycdn.com/mod-garden/";
12+
13+
public static <T> HttpResponse<T> get(String endpoint, HttpResponse.BodyHandler<T> bodyHandler, String... headers) throws IOException, InterruptedException {
14+
var req = HttpRequest.newBuilder(URI.create(API_URL + endpoint))
15+
.header("AccessKey", GardenBot.DOTENV.get("BUNNY_CDN_KEY"));
16+
if (headers.length > 0)
17+
req.headers(headers);
18+
19+
return GardenBot.HTTP_CLIENT.send(req.build(), bodyHandler);
20+
}
21+
22+
public static <T> HttpResponse<T> post(String endpoint, HttpRequest.BodyPublisher bodyPublisher, HttpResponse.BodyHandler<T> bodyHandler, String... headers) throws IOException, InterruptedException {
23+
var req = HttpRequest.newBuilder(URI.create(API_URL + endpoint))
24+
.header("AccessKey", GardenBot.DOTENV.get("BUNNY_CDN_KEY"));
25+
if (headers.length > 0)
26+
req.headers(headers);
27+
req.POST(bodyPublisher);
28+
29+
return GardenBot.HTTP_CLIENT.send(req.build(), bodyHandler);
30+
}
31+
32+
public static <T> HttpResponse<T> put(String endpoint, HttpRequest.BodyPublisher bodyPublisher, HttpResponse.BodyHandler<T> bodyHandler, String... headers) throws IOException, InterruptedException {
33+
var req = HttpRequest.newBuilder(URI.create(API_URL + endpoint))
34+
.header("AccessKey", GardenBot.DOTENV.get("BUNNY_CDN_KEY"));
35+
if (headers.length > 0)
36+
req.headers(headers);
37+
req.PUT(bodyPublisher);
38+
39+
return GardenBot.HTTP_CLIENT.send(req.build(), bodyHandler);
40+
}
41+
}

0 commit comments

Comments
 (0)