diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 507d26736..eb08d7a4c 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -7,6 +7,7 @@ import org.bukkit.Material; import org.bukkit.event.EventPriority; import org.spongepowered.configurate.CommentedConfigurationNode; +import java.io.File; import java.math.BigDecimal; import java.text.NumberFormat; import java.util.List; @@ -17,6 +18,8 @@ import java.util.function.Predicate; import java.util.regex.Pattern; public interface ISettings extends IConf { + File getConfigFile(); + boolean areSignsDisabled(); IText getAnnounceNewPlayerFormat(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Kits.java b/Essentials/src/main/java/com/earth2me/essentials/Kits.java index 5cb419bab..9d8c4639a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Kits.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Kits.java @@ -31,6 +31,10 @@ public class Kits implements IConf { kits = _getKits(); } + public File getFile() { + return config.getFile(); + } + private CommentedConfigurationNode _getKits() { final CommentedConfigurationNode section = config.getSection("kits"); if (section != null) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 0459350ee..4ff5fa447 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -145,6 +145,11 @@ public class Settings implements net.ess3.api.ISettings { reloadConfig(); } + @Override + public File getConfigFile() { + return config.getFile(); + } + @Override public boolean getRespawnAtHome() { return config.getBoolean("respawn-at-home", false); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java index d00c80351..81d4519b5 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java @@ -3,11 +3,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.User; import com.earth2me.essentials.utils.DateUtil; -import com.google.common.base.Charsets; -import com.google.common.io.CharStreams; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; +import com.earth2me.essentials.utils.PasteUtil; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; @@ -17,25 +13,16 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import java.io.BufferedWriter; -import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.StringWriter; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; import static com.earth2me.essentials.I18n.tl; public class Commandcreatekit extends EssentialsCommand { - private static final String PASTE_URL = "https://paste.gg/"; - private static final String PASTE_UPLOAD_URL = "https://api.paste.gg/v1/pastes"; - private static final Gson GSON = new Gson(); - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - public Commandcreatekit() { super("createkit"); } @@ -81,7 +68,7 @@ public class Commandcreatekit extends EssentialsCommand { } private void uploadPaste(final CommandSource sender, final String kitName, final long delay, final List list) { - executorService.submit(() -> { + ess.runTaskAsynchronously(() -> { try { final StringWriter sw = new StringWriter(); final YamlConfigurationLoader loader = YamlConfigurationLoader.builder().sink(() -> new BufferedWriter(sw)).indent(2).nodeStyle(NodeStyle.BLOCK).build(); @@ -95,52 +82,27 @@ public class Commandcreatekit extends EssentialsCommand { final String fileContents = sw.toString(); - final HttpURLConnection connection = (HttpURLConnection) new URL(PASTE_UPLOAD_URL).openConnection(); - connection.setRequestMethod("POST"); - connection.setDoInput(true); - connection.setDoOutput(true); - connection.setRequestProperty("User-Agent", "EssentialsX plugin"); - connection.setRequestProperty("Content-Type", "application/json"); - final JsonObject body = new JsonObject(); - final JsonArray files = new JsonArray(); - final JsonObject file = new JsonObject(); - final JsonObject content = new JsonObject(); - content.addProperty("format", "text"); - content.addProperty("value", fileContents); - file.add("content", content); - files.add(file); - body.add("files", files); - - try (final OutputStream os = connection.getOutputStream()) { - os.write(body.toString().getBytes(Charsets.UTF_8)); - } - // Error - if (connection.getResponseCode() >= 400) { + final CompletableFuture future = PasteUtil.createPaste(Collections.singletonList(new PasteUtil.PasteFile("kit_" + kitName + ".yml", fileContents))); + future.thenAccept(result -> { + if (result != null) { + final String separator = tl("createKitSeparator"); + final String delayFormat = delay <= 0 ? "0" : DateUtil.formatDateDiff(System.currentTimeMillis() + (delay * 1000)); + sender.sendMessage(separator); + sender.sendMessage(tl("createKitSuccess", kitName, delayFormat, result.getPasteUrl())); + sender.sendMessage(separator); + if (ess.getSettings().isDebug()) { + ess.getLogger().info(sender.getSender().getName() + " created a kit: " + result.getPasteUrl()); + } + } + }); + future.exceptionally(throwable -> { sender.sendMessage(tl("createKitFailed", kitName)); - final String message = CharStreams.toString(new InputStreamReader(connection.getErrorStream(), Charsets.UTF_8)); - ess.getLogger().severe("Error creating kit: " + message); - return; - } - - // Read URL - final JsonObject object = GSON.fromJson(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8), JsonObject.class); - final String pasteUrl = PASTE_URL + object.get("result").getAsJsonObject().get("id").getAsString(); - connection.disconnect(); - - final String separator = tl("createKitSeparator"); - String delayFormat = "0"; - if (delay > 0) { - delayFormat = DateUtil.formatDateDiff(System.currentTimeMillis() + (delay * 1000)); - } - sender.sendMessage(separator); - sender.sendMessage(tl("createKitSuccess", kitName, delayFormat, pasteUrl)); - sender.sendMessage(separator); - if (ess.getSettings().isDebug()) { - ess.getLogger().info(sender.getSender().getName() + " created a kit: " + pasteUrl); - } - } catch (final Exception e) { + ess.getLogger().log(Level.SEVERE, "Error creating kit: ", throwable); + return null; + }); + } catch (Exception e) { sender.sendMessage(tl("createKitFailed", kitName)); - e.printStackTrace(); + ess.getLogger().log(Level.SEVERE, "Error creating kit: ", e); } }); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index 64ae147fd..d17d17143 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -10,10 +10,14 @@ import com.earth2me.essentials.utils.DateUtil; import com.earth2me.essentials.utils.EnumUtil; import com.earth2me.essentials.utils.FloatUtil; import com.earth2me.essentials.utils.NumberUtil; +import com.earth2me.essentials.utils.PasteUtil; import com.earth2me.essentials.utils.VersionUtil; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Server; @@ -25,13 +29,22 @@ import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitRunnable; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -103,6 +116,9 @@ public class Commandessentials extends EssentialsCommand { case "commands": runCommands(server, sender, commandLabel, args); break; + case "dump": + runDump(server, sender, commandLabel, args); + break; // Data commands case "reload": @@ -156,6 +172,168 @@ public class Commandessentials extends EssentialsCommand { } } + // Generates a paste of useful information + private void runDump(Server server, CommandSource sender, String commandLabel, String[] args) { + sender.sendMessage(tl("dumpCreating")); + + final JsonObject dump = new JsonObject(); + + final JsonObject meta = new JsonObject(); + meta.addProperty("timestamp", Instant.now().toEpochMilli()); + meta.addProperty("sender", sender.getPlayer() != null ? sender.getPlayer().getName() : null); + meta.addProperty("senderUuid", sender.getPlayer() != null ? sender.getPlayer().getUniqueId().toString() : null); + dump.add("meta", meta); + + final JsonObject serverData = new JsonObject(); + serverData.addProperty("bukkit-version", Bukkit.getBukkitVersion()); + serverData.addProperty("server-version", Bukkit.getVersion()); + serverData.addProperty("server-brand", Bukkit.getName()); + final JsonObject supportStatus = new JsonObject(); + final VersionUtil.SupportStatus status = VersionUtil.getServerSupportStatus(); + supportStatus.addProperty("status", status.name()); + supportStatus.addProperty("supported", status.isSupported()); + supportStatus.addProperty("trigger", VersionUtil.getSupportStatusClass()); + serverData.add("support-status", supportStatus); + dump.add("server-data", serverData); + + final JsonObject environment = new JsonObject(); + environment.addProperty("java-version", System.getProperty("java.version")); + environment.addProperty("operating-system", System.getProperty("os.name")); + environment.addProperty("uptime", DateUtil.formatDateDiff(ManagementFactory.getRuntimeMXBean().getStartTime())); + environment.addProperty("allocated-memory", (Runtime.getRuntime().totalMemory() / 1024 / 1024) + "MB"); + dump.add("environment", environment); + + final JsonObject essData = new JsonObject(); + essData.addProperty("version", ess.getDescription().getVersion()); + final JsonObject updateData = new JsonObject(); + updateData.addProperty("id", ess.getUpdateChecker().getVersionIdentifier()); + updateData.addProperty("branch", ess.getUpdateChecker().getVersionBranch()); + updateData.addProperty("dev", ess.getUpdateChecker().isDevBuild()); + essData.add("update-data", updateData); + final JsonObject econLayer = new JsonObject(); + econLayer.addProperty("enabled", !ess.getSettings().isEcoDisabled()); + econLayer.addProperty("selected-layer", EconomyLayers.isLayerSelected()); + final EconomyLayer layer = EconomyLayers.getSelectedLayer(); + econLayer.addProperty("name", layer == null ? "null" : layer.getName()); + econLayer.addProperty("layer-version", layer == null ? "null" : layer.getPluginVersion()); + econLayer.addProperty("backend-name", layer == null ? "null" : layer.getBackendName()); + essData.add("economy-layer", econLayer); + final JsonArray addons = new JsonArray(); + final JsonArray plugins = new JsonArray(); + final ArrayList alphabetical = new ArrayList<>(); + Collections.addAll(alphabetical, Bukkit.getPluginManager().getPlugins()); + alphabetical.sort(Comparator.comparing(o -> o.getName().toUpperCase(Locale.ENGLISH))); + for (final Plugin plugin : alphabetical) { + final JsonObject pluginData = new JsonObject(); + final PluginDescriptionFile info = plugin.getDescription(); + final String name = info.getName(); + + pluginData.addProperty("name", name); + pluginData.addProperty("version", info.getVersion()); + pluginData.addProperty("description", info.getDescription()); + pluginData.addProperty("main", info.getMain()); + pluginData.addProperty("enabled", plugin.isEnabled()); + pluginData.addProperty("official", plugin == ess || officialPlugins.contains(name)); + pluginData.addProperty("unsupported", warnPlugins.contains(name)); + + final JsonArray authors = new JsonArray(); + info.getAuthors().forEach(authors::add); + pluginData.add("authors", authors); + + if (name.startsWith("Essentials") && !name.equals("Essentials")) { + addons.add(pluginData); + } + plugins.add(pluginData); + } + essData.add("addons", addons); + dump.add("ess-data", essData); + dump.add("plugins", plugins); + + final List files = new ArrayList<>(); + files.add(new PasteUtil.PasteFile("dump.json", dump.toString())); + + final Plugin essDiscord = Bukkit.getPluginManager().getPlugin("EssentialsDiscord"); + + // Further operations will be heavy IO + ess.runTaskAsynchronously(() -> { + boolean config = false; + boolean discord = false; + boolean kits = false; + boolean log = false; + for (final String arg : args) { + if (arg.equals("*")) { + config = true; + discord = true; + kits = true; + log = true; + break; + } else if (arg.equalsIgnoreCase("config")) { + config = true; + } else if (arg.equalsIgnoreCase("discord")) { + discord = true; + } else if (arg.equalsIgnoreCase("kits")) { + kits = true; + } else if (arg.equalsIgnoreCase("log")) { + log = true; + } + } + + if (config) { + try { + files.add(new PasteUtil.PasteFile("config.yml", new String(Files.readAllBytes(ess.getSettings().getConfigFile().toPath()), StandardCharsets.UTF_8))); + } catch (IOException e) { + sender.sendMessage(tl("dumpErrorUpload", "config.yml", e.getMessage())); + } + } + + if (discord && essDiscord != null && essDiscord.isEnabled()) { + try { + files.add(new PasteUtil.PasteFile("discord-config.yml", + new String(Files.readAllBytes(essDiscord.getDataFolder().toPath().resolve("config.yml")), StandardCharsets.UTF_8) + .replaceAll("[MN][A-Za-z\\d]{23}\\.[\\w-]{6}\\.[\\w-]{27}", ""))); + } catch (IOException e) { + sender.sendMessage(tl("dumpErrorUpload", "discord-config.yml", e.getMessage())); + } + } + + if (kits) { + try { + files.add(new PasteUtil.PasteFile("kits.yml", new String(Files.readAllBytes(ess.getKits().getFile().toPath()), StandardCharsets.UTF_8))); + } catch (IOException e) { + sender.sendMessage(tl("dumpErrorUpload", "kits.yml", e.getMessage())); + } + } + + if (log) { + try { + files.add(new PasteUtil.PasteFile("latest.log", new String(Files.readAllBytes(Paths.get("logs", "latest.log")), StandardCharsets.UTF_8) + .replaceAll("(?m)^\\[\\d\\d:\\d\\d:\\d\\d] \\[.+/(?:DEBUG|TRACE)]: .+\\s(?:[A-Za-z.]+:.+\\s(?:\\t.+\\s)*)?\\s*(?:\"[A-Za-z]+\" : .+[\\s}\\]]+)*", "") + .replaceAll("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}", ""))); + } catch (IOException e) { + sender.sendMessage(tl("dumpErrorUpload", "latest.log", e.getMessage())); + } + } + + final CompletableFuture future = PasteUtil.createPaste(files); + future.thenAccept(result -> { + if (result != null) { + final String dumpUrl = "https://essentialsx.net/dump.html?id=" + result.getPasteId(); + sender.sendMessage(tl("dumpUrl", dumpUrl)); + sender.sendMessage(tl("dumpDeleteKey", result.getDeletionKey())); + if (sender.isPlayer()) { + ess.getLogger().info(tl("dumpConsoleUrl", dumpUrl)); + ess.getLogger().info(tl("dumpDeleteKey", result.getDeletionKey())); + } + } + files.clear(); + }); + future.exceptionally(throwable -> { + sender.sendMessage(tl("dumpError", throwable.getMessage())); + return null; + }); + }); + } + // Resets the given player's user data. private void runReset(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception { if (args.length < 2) { @@ -491,6 +669,7 @@ public class Commandessentials extends EssentialsCommand { final List options = Lists.newArrayList(); options.add("reload"); options.add("version"); + options.add("dump"); options.add("commands"); options.add("debug"); options.add("reset"); @@ -534,6 +713,16 @@ public class Commandessentials extends EssentialsCommand { return Lists.newArrayList("ignoreUFCache"); } break; + case "dump": + final List list = Lists.newArrayList("config", "kits", "log", "discord", "*"); + for (String arg : args) { + if (arg.equals("*")) { + list.clear(); + return list; + } + list.remove(arg.toLowerCase(Locale.ENGLISH)); + } + return list; } return Collections.emptyList(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/PasteUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/PasteUtil.java new file mode 100644 index 000000000..4acef1f95 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/PasteUtil.java @@ -0,0 +1,127 @@ +package com.earth2me.essentials.utils; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public final class PasteUtil { + private static final String PASTE_URL = "https://paste.gg/"; + private static final String PASTE_UPLOAD_URL = "https://api.paste.gg/v1/pastes"; + private static final ExecutorService PASTE_EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); + private static final Gson GSON = new Gson(); + + private PasteUtil() { + } + + /** + * Creates an anonymous paste containing the provided files. + * + * @param pages The files to include in the paste. + * @return The result of the paste, including the paste URL and deletion key. + */ + public static CompletableFuture createPaste(List pages) { + final CompletableFuture future = new CompletableFuture<>(); + PASTE_EXECUTOR_SERVICE.submit(() -> { + try { + final HttpURLConnection connection = (HttpURLConnection) new URL(PASTE_UPLOAD_URL).openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setRequestProperty("User-Agent", "EssentialsX plugin"); + connection.setRequestProperty("Content-Type", "application/json"); + final JsonObject body = new JsonObject(); + final JsonArray files = new JsonArray(); + for (final PasteFile page : pages) { + final JsonObject file = new JsonObject(); + final JsonObject content = new JsonObject(); + file.addProperty("name", page.getName()); + content.addProperty("format", "text"); + content.addProperty("value", page.getContents()); + file.add("content", content); + files.add(file); + } + body.add("files", files); + + try (final OutputStream os = connection.getOutputStream()) { + os.write(body.toString().getBytes(Charsets.UTF_8)); + } + + if (connection.getResponseCode() >= 400) { + //noinspection UnstableApiUsage + future.completeExceptionally(new Error(CharStreams.toString(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8)))); + return; + } + + // Read URL + final JsonObject object = GSON.fromJson(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8), JsonObject.class); + final String pasteId = object.get("result").getAsJsonObject().get("id").getAsString(); + final String pasteUrl = PASTE_URL + pasteId; + final JsonElement deletionKey = object.get("result").getAsJsonObject().get("deletion_key"); + connection.disconnect(); + + final PasteResult result = new PasteResult(pasteId, pasteUrl, deletionKey != null ? deletionKey.getAsString() : null); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + public static class PasteFile { + private final String name; + private final String contents; + + public PasteFile(final String name, final String contents) { + this.name = name; + this.contents = contents; + } + + public String getName() { + return name; + } + + public String getContents() { + return contents; + } + } + + public static class PasteResult { + private final String pasteId; + private final String pasteUrl; + private final @Nullable String deletionKey; + + protected PasteResult(String pasteId, final String pasteUrl, final @Nullable String deletionKey) { + this.pasteId = pasteId; + this.pasteUrl = pasteUrl; + this.deletionKey = deletionKey; + } + + public String getPasteUrl() { + return pasteUrl; + } + + public @Nullable String getDeletionKey() { + return deletionKey; + } + + public String getPasteId() { + return pasteId; + } + } + +} diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index c9e7d8fef..1b0498826 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -260,6 +260,12 @@ disposalCommandUsage=/ distance=\u00a76Distance\: {0} dontMoveMessage=\u00a76Teleportation will commence in\u00a7c {0}\u00a76. Don''t move. downloadingGeoIp=Downloading GeoIP database... this might take a while (country\: 1.7 MB, city\: 30MB) +dumpConsoleUrl=A server dump was created: \u00a7c{0} +dumpCreating=\u00a76Creating server dump... +dumpDeleteKey=\u00a76If you want to delete this dump at a later date, use the following deletion key: \u00a7c{0} +dumpError=\u00a74Error while creating dump \u00a7c{0}\u00a74. +dumpErrorUpload=\u00a74Error while uploading \u00a7c{0}\u00a74: \u00a7c{1} +dumpUrl=\u00a76Created server dump: \u00a7c{0} duplicatedUserdata=Duplicated userdata\: {0} and {1}. durability=\u00a76This tool has \u00a7c{0}\u00a76 uses left. east=E @@ -309,6 +315,8 @@ essentialsCommandUsage6=/ cleanup essentialsCommandUsage6Description=Cleans up old userdata essentialsCommandUsage7=/ homes essentialsCommandUsage7Description=Manages user homes +essentialsCommandUsage8=/ dump [*] [config] [discord] [kits] [log] +essentialsCommandUsage8Description=Generates a server dump with the requested information essentialsHelp1=The file is broken and Essentials can''t open it. Essentials is now disabled. If you can''t fix the file yourself, go to http\://tiny.cc/EssentialsChat essentialsHelp2=The file is broken and Essentials can''t open it. Essentials is now disabled. If you can''t fix the file yourself, either type /essentialshelp in game or go to http\://tiny.cc/EssentialsChat essentialsReload=\u00a76Essentials reloaded\u00a7c {0}.