diff --git a/Plan/src/main/java/com/djrapitops/plan/command/commands/ManageCommand.java b/Plan/src/main/java/com/djrapitops/plan/command/commands/ManageCommand.java index e476ac497..8fef36d74 100644 --- a/Plan/src/main/java/com/djrapitops/plan/command/commands/ManageCommand.java +++ b/Plan/src/main/java/com/djrapitops/plan/command/commands/ManageCommand.java @@ -57,5 +57,6 @@ public class ManageCommand extends TreeCommand { commands.add(new ManageRemoveCommand(plugin)); // commands.add(new ManageCleanCommand(plugin)); commands.add(new ManageClearCommand(plugin)); + commands.add(new ManageDumpCommand(plugin)); } } diff --git a/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageClearCommand.java b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageClearCommand.java index 6aba02654..e6d1290a9 100644 --- a/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageClearCommand.java +++ b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageClearCommand.java @@ -30,7 +30,7 @@ public class ManageClearCommand extends SubCommand { * @param plugin Current instance of Plan */ public ManageClearCommand(Plan plugin) { - super("clear", CommandType.CONSOLE_WITH_ARGUMENTS, Permissions.MANAGE.getPermission(), Phrase.CMD_USG_MANAGE_CLEAR + "", " [-a]"); + super("clear", CommandType.CONSOLE_WITH_ARGUMENTS, Permissions.MANAGE.getPermission(), Phrase.CMD_USG_MANAGE_CLEAR.toString(), " [-a]"); this.plugin = plugin; setHelp(plugin); diff --git a/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageDumpCommand.java b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageDumpCommand.java new file mode 100644 index 000000000..c0d0f1577 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageDumpCommand.java @@ -0,0 +1,65 @@ +package main.java.com.djrapitops.plan.command.commands.manage; + +import com.djrapitops.plugin.command.CommandType; +import com.djrapitops.plugin.command.ISender; +import com.djrapitops.plugin.command.SubCommand; +import com.djrapitops.plugin.settings.ColorScheme; +import com.djrapitops.plugin.task.AbsRunnable; +import main.java.com.djrapitops.plan.Permissions; +import main.java.com.djrapitops.plan.Phrase; +import main.java.com.djrapitops.plan.Plan; +import main.java.com.djrapitops.plan.utilities.dump.DumpUtils; + +/** + * This manage subcommand is used to dump important data to pastebin, + * so it's easier to write an issue. + * + * @author Fuzzlemann + * @since 3.7.0 + */ +public class ManageDumpCommand extends SubCommand { + + private final Plan plugin; + + /** + * Class Constructor. + * + * @param plugin Current instance of Plan + */ + public ManageDumpCommand(Plan plugin) { + super("dump", CommandType.CONSOLE, Permissions.MANAGE.getPermission(), Phrase.CMD_USG_MANAGE_CLEAR.toString()); + + this.plugin = plugin; + setHelp(plugin); + } + + private void setHelp(Plan plugin) { + ColorScheme colorScheme = plugin.getColorScheme(); + + String mCol = colorScheme.getMainColor(); + String tCol = colorScheme.getTertiaryColor(); + + String[] help = new String[]{ + mCol + "Manage Dump command", + tCol + " Used to dump important data for bug reporting to hastebin.", + }; + + setInDepthHelp(help); + } + + @Override + public boolean onCommand(ISender sender, String commandLabel, String[] args) { + dump(sender); + return true; + } + + private void dump(ISender sender) { + plugin.getRunnableFactory().createNew(new AbsRunnable("DumpTask") { + @Override + public void run() { + sender.sendLink("Link to the Dump", DumpUtils.dump(plugin)); + sender.sendLink("Report Issues here", "https://github.com/Rsl1122/Plan-PlayerAnalytics/issues/new"); + } + }).runTaskAsynchronously(); + } +} diff --git a/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageImportCommand.java b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageImportCommand.java index e13d5baf7..7d0b6e2ba 100644 --- a/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageImportCommand.java +++ b/Plan/src/main/java/com/djrapitops/plan/command/commands/manage/ManageImportCommand.java @@ -39,6 +39,7 @@ public class ManageImportCommand extends SubCommand { */ public ManageImportCommand(Plan plugin) { super("import", CommandType.CONSOLE, Permissions.MANAGE.getPermission(), Phrase.CMD_USG_MANAGE_IMPORT.toString(), Phrase.ARG_IMPORT.toString()); + this.plugin = plugin; setHelp(plugin); } diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpLog.java b/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpLog.java new file mode 100644 index 000000000..371966564 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpLog.java @@ -0,0 +1,137 @@ +package main.java.com.djrapitops.plan.utilities.dump; + +import main.java.com.djrapitops.plan.Log; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import javax.net.ssl.HttpsURLConnection; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Fuzzlemann + * @since 3.7.0 + */ +public class DumpLog { + + private List lines = new ArrayList<>(); + + /** + * Writes a header + * + * @param header The name of the header + */ + void addHeader(String header) { + addLine(""); + addLine("--- " + header + " ---"); + } + + /** + * Adds a String {@code value} to a String {@code key} + * + * @param key The key + * @param value The value + */ + void add(String key, String value) { + addLine(key + ": " + value); + } + + /** + * Adds a boolean {@code value} to a String {@code key} + * + * @param key The key + * @param value The value + */ + void add(String key, boolean value) { + addLine(key + ": " + value); + } + + /** + * Adds multiple {@link CharSequence CharSequences} stored in an {@link Iterable} + * to a String {@code key} + * + * @param key The key + * @param value The CharSequences stored in an Iterable + */ + void add(String key, Iterable value) { + addLine(key + ": " + String.join(", ", value)); + } + + /** + * Adds multiple lines + * + * @param lines The CharSequences stored in an Iterable + */ + void addLines(Iterable lines) { + lines.forEach(this::addLine); + } + + /** + * Adds multiple lines + * + * @param lines The lines + */ + void addLines(CharSequence... lines) { + Arrays.stream(lines).forEach(this::addLine); + } + + /** + * Adds one line + * + * @param line The content of the line + */ + private void addLine(CharSequence line) { + lines.add(line.toString()); + } + + /** + * Uploads the dump log to Hastebin using HTTPS and POST + * + * @return The link to the Dump Log + */ + String upload() { + String content = this.toString(); + HttpsURLConnection connection = null; + try { + URL url = new URL("https://hastebin.com/documents"); + connection = (HttpsURLConnection) url.openConnection(); + + connection.setRequestProperty("Content-length", String.valueOf(content.length())); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestProperty("User-Agent", "Mozilla/4.0"); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + + DataOutputStream wr = new DataOutputStream(connection.getOutputStream()); + wr.writeBytes(this.toString()); + wr.flush(); + wr.close(); + + BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream())); + JSONParser parser = new JSONParser(); + JSONObject json = (JSONObject) parser.parse(rd.readLine()); + + return "https://hastebin.com/" + json.get("key"); + } catch (IOException | ParseException e) { + Log.toLog("DumpLog.upload", e); + return "Error"; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + @Override + public String toString() { + return String.join("\n", lines); + } +} diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpUtils.java b/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpUtils.java new file mode 100644 index 000000000..5e99f83d8 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/dump/DumpUtils.java @@ -0,0 +1,312 @@ +package main.java.com.djrapitops.plan.utilities.dump; + +import main.java.com.djrapitops.plan.Log; +import main.java.com.djrapitops.plan.Plan; +import main.java.com.djrapitops.plan.Settings; +import org.bukkit.Server; +import org.bukkit.plugin.Plugin; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.nio.charset.Charset; +import java.nio.charset.MalformedInputException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +/** + * @author Fuzzlemann + * @since 3.7.0 + */ +public class DumpUtils { + + /** + * Dumps the following things to Hastebin + *
    + *
  • The current time with the time zone
  • + *
  • The system details
  • + *
  • The server details
  • + *
  • The Plan details
  • + *
  • Some important Configuration details
  • + *
  • The Plan timings
  • + *
  • The error log (if present)
  • + *
  • The debug log (if present)
  • + *
+ * + * @param plugin The Plan instance + * @return The link to the Dump Log + */ + public static String dump(Plan plugin) { + DumpLog log = new DumpLog(); + + addTime(log); + addSystemDetails(log); + addServerDetails(log, plugin); + addPlanDetails(log, plugin); + addConfigurationDetails(log, plugin); + addTimings(log, plugin); + try { + addErrorLog(log, plugin); + addDebugLog(log, plugin); + } catch (IOException e) { + Log.toLog("DumpUtils.dump", e); + return "Error"; + } + + return log.upload(); + } + + /** + * Adds the current time to the Dump log + *

+ * The format of the time is "dd.MM.yyy HH:mm:ss z" + * + * @param log The log + */ + private static void addTime(DumpLog log) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss z"); + String time = simpleDateFormat.format(new Date()); + + log.add("Time", time); + } + + /** + * Adds the following system details to the Dump log + *

    + *
  • The Operating System Name
  • + *
  • The Operating System Version
  • + *
  • The Operating System Architecture
  • + *
  • The Java Vendor
  • + *
  • The Java Version
  • + *
  • The JVM Vendor
  • + *
  • The JVM Version
  • + *
  • The JVM Name
  • + *
  • The JVM Flags
  • + *
+ * + * @param log The log + */ + private static void addSystemDetails(DumpLog log) { + RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); + Properties properties = System.getProperties(); + + String osName = properties.getProperty("os.name"); + String osVersion = properties.getProperty("os.version"); + String osArch = properties.getProperty("os.arch"); + + String javaVendor = properties.getProperty("java.vendor"); + String javaVersion = properties.getProperty("java.version"); + + String javaVMVendor = properties.getProperty("java.vm.vendor"); + String javaVMName = properties.getProperty("java.vm.name"); + String javaVMVersion = properties.getProperty("java.vm.version"); + List javaVMFlags = runtimeMxBean.getInputArguments(); + + log.addHeader("System Details"); + + log.add("Operating System ", osName + " (" + osArch + ") version " + osVersion); + + log.add("Java Version", javaVersion + ", " + javaVendor); + log.add("Java VM Version", javaVMName + " version " + javaVMVersion + ", " + javaVMVendor); + log.add("Java VM Flags", javaVMFlags); + } + + /** + * Adds the following server details to the Dump log + *
    + *
  • The Minecraft Version
  • + *
  • The Server Type
  • + *
  • The installed plugins with the version
  • + *
+ * + * @param log The log + * @param plan The Plan instance + */ + private static void addServerDetails(DumpLog log, Plan plan) { + Server server = plan.getServer(); + + String minecraftVersion = server.getVersion(); + String serverType = server.getName(); + + List plugins = Arrays.stream(server.getPluginManager().getPlugins()) + .map(Plugin::getDescription) + .map(description -> description.getName() + " " + description.getVersion()) + .sorted() + .collect(Collectors.toList()); + + log.addHeader("Server Details"); + + log.add("Minecraft Version", minecraftVersion); + log.add("Server Type", serverType); + + log.addHeader("Plugins"); + log.addLines(plugins); + } + + /** + * Adds the following Plan details to the Dump log + *
    + *
  • The Plan Version
  • + *
  • The Abstract Plugin Framework Version
  • + *
+ * + * @param log The log + * @param plan The Plan instance + */ + private static void addPlanDetails(DumpLog log, Plan plan) { + String planVersion = plan.getVersion(); + String apfVersion = plan.getAPFVersion(); + + log.addHeader("Plan Details"); + + log.add("Plan Version", planVersion); + log.add("Abstract Plugin Framework Version", apfVersion); + } + + /** + * Adds the following Configuration Details to the Dump Log + *
    + *
  • WebServer enabled
  • + *
  • HTTPS used
  • + *
  • Analysis on enable refresh
  • + *
  • Analysis Export
  • + *
  • Alternative Server IP usage
  • + *
  • Chat Gathering
  • + *
  • Kill Gathering
  • + *
  • Command Gathering
  • + *
  • Alias Combining
  • + *
  • Unknown Command Logging
  • + *
  • The locale
  • + *
  • The DB Type
  • + *
+ * + * @param log The log + * @param plan The Plan instance + */ + private static void addConfigurationDetails(DumpLog log, Plan plan) { + boolean webServerEnabled = Settings.WEBSERVER_ENABLED.isTrue(); + boolean usingHTTPS = plan.getUiServer().usingHttps(); + boolean refreshAnalysisOnEnable = Settings.ANALYSIS_REFRESH_ON_ENABLE.isTrue(); + boolean analysisExport = Settings.ANALYSIS_EXPORT.isTrue(); + boolean usingAlternativeServerIP = Settings.USE_ALTERNATIVE_UI.isTrue(); + + boolean chatGathering = Settings.GATHERCHAT.isTrue(); + boolean killGathering = Settings.GATHERKILLS.isTrue(); + boolean commandGathering = Settings.GATHERCOMMANDS.isTrue(); + + boolean combineAliases = Settings.COMBINE_COMMAND_ALIASES_TO_MAIN_COMMAND.isTrue(); + boolean unknownCommandLogging = Settings.DO_NOT_LOG_UNKNOWN_COMMANDS.isTrue(); + + String locale = Settings.LOCALE.toString(); + String dbType = Settings.DB_TYPE.toString(); + + log.addHeader("Plan Configuration"); + + log.add("Webserver Enabled", webServerEnabled); + log.add("Webserver HTTPS", usingHTTPS); + log.add("Refresh Analysis on Enable", refreshAnalysisOnEnable); + log.add("Analysis Export", analysisExport); + log.add("Alternative Server IP", usingAlternativeServerIP); + + log.add("Chat Gathering", chatGathering); + log.add("Kill Gathering", killGathering); + log.add("Command Gathering", commandGathering); + + log.add("Combine Aliases", combineAliases); + log.add("Unknown Command Logging", unknownCommandLogging); + log.add("Locale", locale); + + log.add("Database Type", dbType); + } + + /** + * Adds the timings to the Dump log + * + * @param log The log + * @param plan The Plan instance + */ + private static void addTimings(DumpLog log, Plan plan) { + String[] timings = plan.benchmark().getTimings().getTimings(); + + log.addHeader("Timings"); + log.addLines(timings); + } + + /** + * Adds the error log to the Dump Log if present + * + * @param log The log + * @param plan The Plan instance + * @throws IOException when an error while reading occurred + */ + private static void addErrorLog(DumpLog log, Plan plan) throws IOException { + Path errorFile = FileSystems.getDefault().getPath(plan.getDataFolder().getAbsolutePath(), Log.getErrorsFilename()); + + if (Files.notExists(errorFile)) { + return; + } + + List lines = readLines(errorFile); + + log.addHeader("Error Log"); + log.addLines(lines); + } + + /** + * Adds the debug log to the Dump Log if present + * + * @param log The log + * @param plan The Plan instance + * @throws IOException when an error while reading occurred + */ + private static void addDebugLog(DumpLog log, Plan plan) throws IOException { + Path debugFile = FileSystems.getDefault().getPath(plan.getDataFolder().getAbsolutePath(), "DebugLog.txt"); + + if (Files.notExists(debugFile)) { + return; + } + + List lines = readLines(debugFile); + + log.addHeader("Debug Log"); + log.addLines(lines); + } + + /** + * Reads the lines of a file + * + * @param file The file + * @return The lines + * @throws IOException when an error while reading occurred + */ + private static List readLines(Path file) throws IOException { + for (Charset charset : Charset.availableCharsets().values()) { + try { + return readLines(file, charset); + } catch (MalformedInputException ignored) { + /* Ignores MalformedInputException, just trying the next charset */ + } + } + + return null; + } + + /** + * Reads the lines of a file with that specific charset + * + * @param file The file + * @param charset The CharSet + * @return The lines + * @throws IOException when an error while reading occurred + */ + private static List readLines(Path file, Charset charset) throws IOException { + return Files.lines(file, charset).collect(Collectors.toList()); + } +} diff --git a/Plan/src/main/resources/analysis.html b/Plan/src/main/resources/analysis.html index d0b519f9f..a6e261922 100644 --- a/Plan/src/main/resources/analysis.html +++ b/Plan/src/main/resources/analysis.html @@ -152,7 +152,7 @@ display: inline-block; border: solid #348e0f; padding: 8px 14px; - border-radius: 3px; + border-radius: 3px; width: 95%; } .header-icon { @@ -669,399 +669,399 @@ @@ -1086,12 +1086,12 @@ var myChart = Highcharts.chart('worldPie', { chart: { plotBackgroundColor: null, - plotBorderWidth: null, - plotShadow: false, + plotBorderWidth: null, + plotShadow: false, type: 'pie' }, title: {text: 'World Playtime'}, - subtitle: {text: '%worldtotal%'}, + subtitle: {text: '%worldtotal%'}, tooltip: { pointFormat: '{series.name}: {point.percentage:.2f}%' }, @@ -1111,14 +1111,14 @@ + + - -