|
1 | 1 | package com.zenith.command.impl; |
2 | 2 |
|
3 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; |
4 | | -import com.zenith.command.api.Command; |
5 | | -import com.zenith.command.api.CommandCategory; |
6 | | -import com.zenith.command.api.CommandContext; |
7 | | -import com.zenith.command.api.CommandUsage; |
| 4 | +import com.zenith.command.api.*; |
8 | 5 | import com.zenith.discord.DiscordBot; |
9 | 6 | import com.zenith.feature.api.Api; |
10 | 7 | import com.zenith.plugin.PluginManager; |
11 | 8 | import com.zenith.plugin.api.PluginInfo; |
12 | 9 | import com.zenith.util.ImageInfo; |
13 | 10 | import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec; |
| 11 | +import org.jspecify.annotations.Nullable; |
14 | 12 |
|
| 13 | +import java.io.File; |
15 | 14 | import java.net.MalformedURLException; |
16 | 15 | import java.net.URI; |
17 | 16 | import java.net.URL; |
18 | 17 | import java.net.http.HttpRequest; |
19 | 18 | import java.net.http.HttpResponse; |
| 19 | +import java.nio.file.FileSystems; |
20 | 20 | import java.nio.file.Files; |
21 | 21 | import java.nio.file.StandardOpenOption; |
| 22 | +import java.util.Collections; |
22 | 23 | import java.util.Comparator; |
| 24 | +import java.util.List; |
23 | 25 | import java.util.Objects; |
24 | 26 | import java.util.stream.Collectors; |
25 | 27 |
|
@@ -56,7 +58,10 @@ public CommandUsage commandUsage() { |
56 | 58 |
|
57 | 59 | @Override |
58 | 60 | public LiteralArgumentBuilder<CommandContext> register() { |
59 | | - return command("plugins").requires(Command::validateAccountOwner) |
| 61 | + return command("plugins") |
| 62 | + .requires(c -> Command.validateAccountOwner(c) |
| 63 | + // todo: consider blocking discord source by default, overridable by config |
| 64 | + && Command.validateCommandSource(c, List.of(CommandSources.TERMINAL, CommandSources.DISCORD))) |
60 | 65 | .then(argument("toggle", toggle()).executes(c -> { |
61 | 66 | CONFIG.plugins.enabled = getToggle(c, "toggle"); |
62 | 67 | c.getSource().getEmbed() |
@@ -115,15 +120,38 @@ public LiteralArgumentBuilder<CommandContext> register() { |
115 | 120 | return ERROR; |
116 | 121 | } |
117 | 122 | var api = new PluginDownloadApi(); |
118 | | - if (!api.download(url)) { |
| 123 | + var downloadResult = api.download(url); |
| 124 | + if (!downloadResult.success()) { |
119 | 125 | c.getSource().getEmbed() |
120 | 126 | .title("Download Failed") |
121 | | - .description("More info may be in ZenithProxy logs"); |
| 127 | + .description(downloadResult.error()); |
| 128 | + if (downloadResult.file() != null) downloadResult.file().delete(); |
122 | 129 | return ERROR; |
123 | 130 | } |
| 131 | + var readResult = readPluginInfo(downloadResult.file()); |
| 132 | + if (!readResult.success()) { |
| 133 | + c.getSource().getEmbed() |
| 134 | + .title("Invalid Plugin Jar") |
| 135 | + .description(readResult.error()); |
| 136 | + downloadResult.file().delete(); |
| 137 | + return ERROR; |
| 138 | + } |
| 139 | + var pluginId = readResult.pluginInfo().id(); |
| 140 | + var existingPlugin = PLUGIN_MANAGER.getPluginInstance(pluginId); |
| 141 | + String desc = "Restart ZenithProxy to reload plugins: `restart`"; |
| 142 | + if (existingPlugin != null) { |
| 143 | + existingPlugin.getJarPath().toFile().deleteOnExit(); |
| 144 | + desc += "\n\nExisting plugin with ID: `%s` found. It will be replaced/updated on next restart.".formatted(pluginId); |
| 145 | + } |
124 | 146 | c.getSource().getEmbed() |
125 | | - .title("Jar Downloaded") |
126 | | - .description(appendWarningToDescription("Restart ZenithProxy to reload plugins: `restart`")) |
| 147 | + .title("Plugin Downloaded") |
| 148 | + .description(appendWarningToDescription(desc)) |
| 149 | + .addField("ID", pluginId) |
| 150 | + .addField("Description", readResult.pluginInfo().description()) |
| 151 | + .addField("Version", readResult.pluginInfo().version()) |
| 152 | + .addField("URL", readResult.pluginInfo().url()) |
| 153 | + .addField("Author(s)", String.join(", ", readResult.pluginInfo().authors())) |
| 154 | + .addField("Jar", downloadResult.file().toPath().getFileName()) |
127 | 155 | .primaryColor(); |
128 | 156 | return OK; |
129 | 157 | }))) |
@@ -158,33 +186,59 @@ private String appendWarningToDescription(String description) { |
158 | 186 | return description; |
159 | 187 | } |
160 | 188 |
|
| 189 | + private PluginInfoReadResult readPluginInfo(File jarFile) { |
| 190 | + var zipUri = URI.create("jar:file:" + jarFile.toURI().getPath()); |
| 191 | + try (var fs = FileSystems.newFileSystem(zipUri, Collections.emptyMap())) { |
| 192 | + var root = fs.getPath("/"); |
| 193 | + var pluginJson = root.resolve("zenithproxy.plugin.json"); |
| 194 | + if (!Files.exists(pluginJson)) { |
| 195 | + return new PluginInfoReadResult(false, "No zenithproxy.plugin.json found in jar", null); |
| 196 | + } |
| 197 | + // should never be larger than a few kb |
| 198 | + if (Files.size(pluginJson) > 100 * 1024) { |
| 199 | + return new PluginInfoReadResult(false, "zenithproxy.plugin.json is too large", null); |
| 200 | + } |
| 201 | + var jsonString = Files.readString(pluginJson); |
| 202 | + var pluginInfo = OBJECT_MAPPER.readValue(jsonString, PluginInfo.class); |
| 203 | + return new PluginInfoReadResult(true, null, pluginInfo); |
| 204 | + } catch (Exception e) { |
| 205 | + return new PluginInfoReadResult(false, e.getMessage(), null); |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + record PluginInfoReadResult(boolean success, @Nullable String error, @Nullable PluginInfo pluginInfo) {} |
| 210 | + |
161 | 211 | private static class PluginDownloadApi extends Api { |
162 | 212 |
|
163 | 213 | public PluginDownloadApi() { |
164 | 214 | super(""); |
165 | 215 | } |
166 | 216 |
|
167 | | - public boolean download(URL url) { |
| 217 | + public PluginDownloadResult download(URL url) { |
168 | 218 | HttpRequest request = buildBaseRequest(url.toString()) |
169 | 219 | .GET() |
170 | 220 | .build(); |
| 221 | + File resFile = null; |
171 | 222 | try (var client = buildHttpClient()) { |
172 | 223 | var response = client |
173 | 224 | .send(request, HttpResponse.BodyHandlers.ofFileDownload(PluginManager.PLUGINS_PATH, StandardOpenOption.CREATE, StandardOpenOption.WRITE)); |
174 | 225 | if (response.statusCode() >= 400) { |
175 | 226 | PLUGIN_LOG.error("Failed to download plugin from: {} - {}", url, response.statusCode()); |
176 | | - return false; |
| 227 | + return new PluginDownloadResult(false, "Failed to download plugin, HTTP error code: %s".formatted(url, response.statusCode()), null); |
177 | 228 | } |
178 | 229 | // verify the jar was written to file |
179 | 230 | if (!Files.exists(response.body())) { |
180 | 231 | PLUGIN_LOG.error("Failed to download plugin from: {} - File not written", url); |
181 | | - return false; |
| 232 | + return new PluginDownloadResult(false, "Failed to download plugin, file not written", null); |
182 | 233 | } |
183 | | - return true; |
| 234 | + resFile = response.body().toFile(); |
| 235 | + return new PluginDownloadResult(true, null, resFile); |
184 | 236 | } catch (Exception e) { |
185 | 237 | PLUGIN_LOG.error("Failed to download plugin from: {} - {}", url, e.getMessage()); |
| 238 | + return new PluginDownloadResult(false, "Failed to download plugin, %s".formatted(e.getMessage()), resFile); |
186 | 239 | } |
187 | | - return false; |
188 | 240 | } |
189 | 241 | } |
| 242 | + |
| 243 | + record PluginDownloadResult(boolean success, @Nullable String error, @Nullable File file) { } |
190 | 244 | } |
0 commit comments