diff options
| author | Luck <git@lucko.me> | 2019-04-28 15:37:32 +0100 |
|---|---|---|
| committer | Luck <git@lucko.me> | 2019-05-04 23:22:32 +0100 |
| commit | c3ae37e88f967a21522af7d0cb79a571326cd7e9 (patch) | |
| tree | 7ce46c61407dacdff3cabaeb1ffc8eb5e7cd614a | |
| parent | 51fa2b3e64f021c3c0535f9f931d3fae27ca7adc (diff) | |
| download | spark-c3ae37e88f967a21522af7d0cb79a571326cd7e9.tar.gz spark-c3ae37e88f967a21522af7d0cb79a571326cd7e9.tar.bz2 spark-c3ae37e88f967a21522af7d0cb79a571326cd7e9.zip | |
Start implementing activity log feature
31 files changed, 903 insertions, 265 deletions
diff --git a/spark-bukkit/build.gradle b/spark-bukkit/build.gradle index e8c6721..836e389 100644 --- a/spark-bukkit/build.gradle +++ b/spark-bukkit/build.gradle @@ -1,6 +1,8 @@ dependencies { compile project(':spark-common') - compile 'net.kyori:text-adapter-bukkit:1.0.3' + compile('net.kyori:text-adapter-bukkit:1.0.3') { + exclude(module: 'text') + } compileOnly 'org.spigotmc:spigot-api:1.12.2-R0.1-SNAPSHOT' } diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitCommandSender.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitCommandSender.java new file mode 100644 index 0000000..dacea76 --- /dev/null +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitCommandSender.java @@ -0,0 +1,61 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) <luck@lucko.me> + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package me.lucko.spark.bukkit; + +import me.lucko.spark.common.CommandSender; +import net.kyori.text.Component; +import net.kyori.text.adapter.bukkit.TextAdapter; + +public class BukkitCommandSender implements CommandSender { + private final org.bukkit.command.CommandSender sender; + + public BukkitCommandSender(org.bukkit.command.CommandSender sender) { + this.sender = sender; + } + + @Override + public String getName() { + return this.sender.getName(); + } + + @Override + public void sendMessage(Component message) { + TextAdapter.sendComponent(this.sender, message); + } + + @Override + public boolean hasPermission(String permission) { + return this.sender.hasPermission(permission); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BukkitCommandSender that = (BukkitCommandSender) o; + return this.sender.equals(that.sender); + } + + @Override + public int hashCode() { + return this.sender.hashCode(); + } +} diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/SparkBukkitPlugin.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/SparkBukkitPlugin.java index 15c725d..ee85c70 100644 --- a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/SparkBukkitPlugin.java +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/SparkBukkitPlugin.java @@ -20,29 +20,27 @@ package me.lucko.spark.bukkit; +import me.lucko.spark.common.CommandSender; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.command.CommandResponseHandler; import me.lucko.spark.common.monitor.tick.TpsCalculator; import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.TickCounter; -import net.kyori.text.Component; import net.kyori.text.TextComponent; -import net.kyori.text.adapter.bukkit.TextAdapter; import org.bukkit.ChatColor; import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; import org.bukkit.plugin.java.JavaPlugin; import java.nio.file.Path; -import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -public class SparkBukkitPlugin extends JavaPlugin implements SparkPlugin<CommandSender> { +public class SparkBukkitPlugin extends JavaPlugin implements SparkPlugin { - private final SparkPlatform<CommandSender> platform = new SparkPlatform<>(this); + private final SparkPlatform platform = new SparkPlatform(this); @Override public void onEnable() { @@ -56,7 +54,7 @@ public class SparkBukkitPlugin extends JavaPlugin implements SparkPlugin<Command return true; } - CommandResponseHandler<CommandSender> resp = new CommandResponseHandler<>(this.platform, sender); + CommandResponseHandler resp = new CommandResponseHandler(this.platform, new BukkitCommandSender(sender)); TpsCalculator tpsCalculator = this.platform.getTpsCalculator(); resp.replyPrefixed(TextComponent.of("TPS from last 5s, 10s, 1m, 5m, 15m:")); resp.replyPrefixed(TextComponent.builder(" ").append(tpsCalculator.toFormattedComponent()).build()); @@ -71,22 +69,14 @@ public class SparkBukkitPlugin extends JavaPlugin implements SparkPlugin<Command } @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!sender.hasPermission("spark")) { - sender.sendMessage(ChatColor.RED + "You do not have permission to use this command."); - return true; - } - - this.platform.executeCommand(sender, args); + public boolean onCommand(org.bukkit.command.CommandSender sender, Command command, String label, String[] args) { + this.platform.executeCommand(new BukkitCommandSender(sender), args); return true; } @Override - public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - if (!sender.hasPermission("spark")) { - return Collections.emptyList(); - } - return this.platform.tabCompleteCommand(sender, args); + public List<String> onTabComplete(org.bukkit.command.CommandSender sender, Command command, String alias, String[] args) { + return this.platform.tabCompleteCommand(new BukkitCommandSender(sender), args); } @Override @@ -106,15 +96,10 @@ public class SparkBukkitPlugin extends JavaPlugin implements SparkPlugin<Command @Override public Set<CommandSender> getSendersWithPermission(String permission) { - Set<CommandSender> senders = new HashSet<>(getServer().getOnlinePlayers()); + List<org.bukkit.command.CommandSender> senders = new LinkedList<>(getServer().getOnlinePlayers()); senders.removeIf(sender -> !sender.hasPermission(permission)); senders.add(getServer().getConsoleSender()); - return senders; - } - - @Override - public void sendMessage(CommandSender sender, Component message) { - TextAdapter.sendComponent(sender, message); + return senders.stream().map(BukkitCommandSender::new).collect(Collectors.toSet()); } @Override diff --git a/spark-bungeecord/build.gradle b/spark-bungeecord/build.gradle index 55dcb4e..5ee50d7 100644 --- a/spark-bungeecord/build.gradle +++ b/spark-bungeecord/build.gradle @@ -1,6 +1,8 @@ dependencies { compile project(':spark-common') - compile 'net.kyori:text-adapter-bungeecord:1.0.3' + compile('net.kyori:text-adapter-bungeecord:1.0.3') { + exclude(module: 'text') + } compileOnly 'net.md-5:bungeecord-api:1.12-SNAPSHOT' } diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordCommandSender.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordCommandSender.java new file mode 100644 index 0000000..d3a831c --- /dev/null +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordCommandSender.java @@ -0,0 +1,61 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) <luck@lucko.me> + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package me.lucko.spark.bungeecord; + +import me.lucko.spark.common.CommandSender; +import net.kyori.text.Component; +import net.kyori.text.adapter.bungeecord.TextAdapter; + +public class BungeeCordCommandSender implements CommandSender { + private final net.md_5.bungee.api.CommandSender sender; + + public BungeeCordCommandSender(net.md_5.bungee.api.CommandSender sender) { + this.sender = sender; + } + + @Override + public String getName() { + return this.sender.getName(); + } + + @Override + public void sendMessage(Component message) { + TextAdapter.sendComponent(this.sender, message); + } + + @Override + public boolean hasPermission(String permission) { + return this.sender.hasPermission(permission); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BungeeCordCommandSender that = (BungeeCordCommandSender) o; + return this.sender.equals(that.sender); + } + + @Override + public int hashCode() { + return this.sender.hashCode(); + } +} diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/SparkBungeeCordPlugin.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/SparkBungeeCordPlugin.java index ba2ee99..193ae06 100644 --- a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/SparkBungeeCordPlugin.java +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/SparkBungeeCordPlugin.java @@ -20,27 +20,24 @@ package me.lucko.spark.bungeecord; +import me.lucko.spark.common.CommandSender; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.TickCounter; -import net.kyori.text.Component; -import net.kyori.text.adapter.bungeecord.TextAdapter; -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.TabExecutor; import java.nio.file.Path; -import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -public class SparkBungeeCordPlugin extends Plugin implements SparkPlugin<CommandSender> { +public class SparkBungeeCordPlugin extends Plugin implements SparkPlugin { - private final SparkPlatform<CommandSender> platform = new SparkPlatform<>(this); + private final SparkPlatform platform = new SparkPlatform(this); @Override public void onEnable() { @@ -70,15 +67,10 @@ public class SparkBungeeCordPlugin extends Plugin implements SparkPlugin<Command @Override public Set<CommandSender> getSendersWithPermission(String permission) { - Set<CommandSender> senders = new HashSet<>(getProxy().getPlayers()); + List<net.md_5.bungee.api.CommandSender> senders = new LinkedList<>(getProxy().getPlayers()); senders.removeIf(sender -> !sender.hasPermission(permission)); senders.add(getProxy().getConsole()); - return senders; - } - - @Override - public void sendMessage(CommandSender sender, Component message) { - TextAdapter.sendComponent(sender, message); + return senders.stream().map(BungeeCordCommandSender::new).collect(Collectors.toSet()); } @Override @@ -105,23 +97,13 @@ public class SparkBungeeCordPlugin extends Plugin implements SparkPlugin<Command } @Override - public void execute(CommandSender sender, String[] args) { - if (!sender.hasPermission("spark")) { - TextComponent msg = new TextComponent("You do not have permission to use this command."); - msg.setColor(ChatColor.RED); - sender.sendMessage(msg); - return; - } - - this.plugin.platform.executeCommand(sender, args); + public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) { + this.plugin.platform.executeCommand(new BungeeCordCommandSender(sender), args); } @Override - public Iterable<String> onTabComplete(CommandSender sender, String[] args) { - if (!sender.hasPermission("spark")) { - return Collections.emptyList(); - } - return this.plugin.platform.tabCompleteCommand(sender, args); + public Iterable<String> onTabComplete(net.md_5.bungee.api.CommandSender sender, String[] args) { + return this.plugin.platform.tabCompleteCommand(new BungeeCordCommandSender(sender), args); } } } diff --git a/spark-common/build.gradle b/spark-common/build.gradle index a8b95c6..8a9ee16 100644 --- a/spark-common/build.gradle +++ b/spark-common/build.gradle @@ -1,7 +1,16 @@ dependencies { compile 'com.squareup.okhttp3:okhttp:3.14.1' compile 'com.squareup.okio:okio:1.17.3' - compile 'net.kyori:text-api:2.0.0' + compile('net.kyori:text-api:2.0.0') { + exclude(module: 'checker-qual') + } + compile('net.kyori:text-serializer-gson:2.0.0') { + exclude(module: 'text-api') + exclude(module: 'gson') + } + compile('net.kyori:text-serializer-legacy:2.0.0') { + exclude(module: 'text-api') + } compileOnly 'com.google.code.gson:gson:2.7' compileOnly 'com.google.guava:guava:19.0' } diff --git a/spark-common/src/main/java/me/lucko/spark/common/ActivityLog.java b/spark-common/src/main/java/me/lucko/spark/common/ActivityLog.java new file mode 100644 index 0000000..4b5e942 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/ActivityLog.java @@ -0,0 +1,195 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) <luck@lucko.me> + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package me.lucko.spark.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class ActivityLog { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final JsonParser PARSER = new JsonParser(); + + private final Path file; + + private final List<Activity> log = new LinkedList<>(); + private final Object[] mutex = new Object[0]; + + public ActivityLog(Path file) { + this.file = file; + } + + public void addToLog(Activity activity) { + synchronized (this.mutex) { + this.log.add(activity); + } + save(); + } + + public List<Activity> getLog() { + synchronized (this.mutex) { + return new LinkedList<>(this.log); + } + } + + public void save() { + JsonArray array = new JsonArray(); + synchronized (this.mutex) { + for (Activity activity : this.log) { + if (!activity.shouldExpire()) { + array.add(activity.serialize()); + } + } + } + + try { + Files.createDirectories(this.file.getParent()); + } catch (IOException e) { + // ignore + } + + try (BufferedWriter writer = Files.newBufferedWriter(this.file, StandardCharsets.UTF_8)) { + GSON.toJson(array, writer); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void load() { + if (!Files.exists(this.file)) { + synchronized (this.mutex) { + this.log.clear(); + return; + } + } + + JsonArray array; + try (BufferedReader reader = Files.newBufferedReader(this.file, StandardCharsets.UTF_8)) { + array = PARSER.parse(reader).getAsJsonArray(); + } catch (IOException e) { + e.printStackTrace(); + return; + } + + boolean save = false; + + synchronized (this.mutex) { + this.log.clear(); + for (JsonElement element : array) { + try { + Activity activity = Activity.deserialize(element); + if (activity.shouldExpire()) { + save = true; + } + this.log.add(activity); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + if (save) { + try { + save(); + } catch (Exception e) { + // ignore + } + } + } + + public static final class Activity { + private final String user; + private final long time; + private final String type; + private final String url; + + public Activity(String user, long time, String type, String url) { + this.user = user; + this.time = time; + this.type = type; + this.url = url; + } + + public String getUser() { + return this.user; + } + + public long getTime() { + return this.time; + } + + public String getType() { + return this.type; + } + + public String getUrl() { + return this.url; + } + + public boolean shouldExpire() { + return (System.currentTimeMillis() - this.time) > TimeUnit.DAYS.toMillis(7); + } + + public JsonObject serialize() { + JsonObject object = new JsonObject(); + + JsonObject user = new JsonObject(); + user.add("name", new JsonPrimitive(this.user)); + object.add("user", user); + + object.add("time", new JsonPrimitive(this.time)); + object.add("type", new JsonPrimitive(this.type)); + + JsonObject data = new JsonObject(); + data.add("url", new JsonPrimitive(this.url)); + object.add("data", data); + + return object; + } + + public static Activity deserialize(JsonElement element) { + JsonObject object = element.getAsJsonObject(); + + String user = object.get("user").getAsJsonObject().get("name").getAsJsonPrimitive().getAsString(); + long time = object.get("time").getAsJsonPrimitive().getAsLong(); + String type = object.get("type").getAsJsonPrimitive().getAsString(); + String url = object.get("data").getAsJsonObject().get("url").getAsJsonPrimitive().getAsString(); + + return new Activity(user, time, type, url); + } + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/CommandSender.java b/spark-common/src/main/java/me/lucko/spark/common/CommandSender.java new file mode 100644 index 0000000..9d2ecc0 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/CommandSender.java @@ -0,0 +1,33 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) <luck@lucko.me> + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package me.lucko.spark.common; + +import net.kyori.text.Component; + +public interface CommandSender { + + String getName(); + + void sendMessage(Component message); + + boolean hasPermission(String permission); + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index 6b3eb21..d05d9e8 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import me.lucko.spark.common.command.Arguments; import me.lucko.spark.common.command.Command; import me.lucko.spark.common.command.CommandResponseHandler; +import me.lucko.spark.common.command.modules.ActivityLogModule; import me.lucko.spark.common.command.modules.HealthModule; import me.lucko.spark.common.command.modules.MemoryModule; import me.lucko.spark.common.command.modules.SamplerModule; @@ -35,6 +36,7 @@ import me.lucko.spark.common.sampler.TickCounter; import me.lucko.spark.common.util.BytebinClient; import net.kyori.text.Component; import net.kyori.text.TextComponent; +import net.kyori.text.event.ClickEvent; import net.kyori.text.format.TextColor; import net.kyori.text.format.TextDecoration; import okhttp3.OkHttpClient; @@ -47,10 +49,8 @@ import java.util.stream.Collectors; /** * Abstract spark implementation used by all platforms. - * - * @param <S> the sender (e.g. CommandSender) type used by the platform */ -public class SparkPlatform<S> { +public class SparkPlatform { /** The URL of the viewer frontend */ public static final String VIEWER_URL = "https://sparkprofiler.github.io/#"; @@ -59,22 +59,26 @@ public class SparkPlatform<S> { /** The bytebin instance used by the platform */ public static final BytebinClient BYTEBIN_CLIENT = new BytebinClient(OK_HTTP_CLIENT, "https://bytebin.lucko.me/", "spark-plugin"); - private final List<Command<S>> commands; - private final SparkPlugin<S> plugin; - + private final SparkPlugin plugin; + private final List<Command> commands; + private final ActivityLog activityLog; private final TickCounter tickCounter; private final TpsCalculator tpsCalculator; - public SparkPlatform(SparkPlugin<S> plugin) { + public SparkPlatform(SparkPlugin plugin) { this.plugin = plugin; - ImmutableList.Builder<Command<S>> commandsBuilder = ImmutableList.builder(); - new SamplerModule<S>().registerCommands(commandsBuilder::add); - new HealthModule<S>().registerCommands(commandsBuilder::add); - new TickMonitoringModule<S>().registerCommands(commandsBuilder::add); - new MemoryModule<S>().registerCommands(commandsBuilder::add); + ImmutableList.Builder<Command> commandsBuilder = ImmutableList.builder(); + new SamplerModule().registerCommands(commandsBuilder::add); + new HealthModule().registerCommands(commandsBuilder::add); + new TickMonitoringModule().registerCommands(commandsBuilder::add); + new MemoryModule().registerCommands(commandsBuilder::add); + new ActivityLogModule().registerCommands(commandsBuilder::add); this.commands = commandsBuilder.build(); + this.activityLog = new ActivityLog(plugin.getPluginFolder().resolve("activity.json")); + this.activityLog.load(); + this.tickCounter = plugin.createTickCounter(); this.tpsCalculator = this.tickCounter != null ? new TpsCalculator() : null; } @@ -92,10 +96,14 @@ public class SparkPlatform<S> { } } - public SparkPlugin<S> getPlugin() { + public SparkPlugin getPlugin() { return this.plugin; } + public ActivityLog getActivityLog() { + return this.activityLog; + } + public TickCounter getTickCounter() { return this.tickCounter; } @@ -104,17 +112,39 @@ public class SparkPlatform<S> { return this.tpsCalculator; } - public void executeCommand(S sender, String[] args) { - CommandResponseHandler<S> resp = new CommandResponseHandler<>(this, sender); + public void executeCommand(CommandSender sender, String[] args) { + CommandResponseHandler resp = new CommandResponseHandler(this, sender); + + if (!sender.hasPermission("spark")) { + resp.replyPrefixed(TextComponent.of("You do not have permission to use this command.", TextColor.RED)); + return; + } + if (args.length == 0) { - sendUsage(resp); + resp.replyPrefixed(TextComponent.builder("") + .append(TextComponent.of("spark", TextColor.WHITE)) + .append(Component.space()) + .append(TextComponent.of("v" + getPlugin().getVersion(), TextColor.GRAY)) + .build() + ); + resp.replyPrefixed(TextComponent.builder(" |
