Compare commits

..
10 Commits
Author SHA1 Message Date
Jones 4e4423e2ce Release 2.0.2 (#45) 2023-10-04 14:58:36 +02:00
Jones ddb65f2f5a Bump dependencies (#44)
* build(deps): bump netty from 4.1.98.Final to 4.1.99.Final

* build(deps): bump gradle from 8.2 to 8.3
2023-10-04 14:53:47 +02:00
Jones 254faecc90 Fix verbose animation being unordered and modifiable (#43)
* fix: make sure the animations are stored in a List, not a Collection

* fix: make animations unmodifiable after reading them from the config
2023-10-04 14:41:17 +02:00
Jones 582024eba1 Improve configuration style and remove unused values (#42)
* feat: use semantic field names for the configuration values

* fix: remove unused messages from the configuration

* feat: use lombok getter for all configuration values
2023-10-04 14:37:29 +02:00
Jones e1fe738ffe Make verbose message handler customizable (#41)
* feat: allow custom verbose handler via the API

* fix: allow modifications of the next action bar message

* fix: don't allow modifications of internal methods
2023-10-04 14:00:16 +02:00
Jones 5f3a439113 Fix permission message not showing on Velocity (#40)
* feat: move permission check to the command execution (https://github.com/jonesdevelopment/sonar/issues/39)

* fix: do not allow tab completion if the player does not have the required permission

* fix: handle https://github.com/jonesdevelopment/sonar-legacy/issues/7 using an environment variable
2023-10-04 12:47:59 +02:00
Jones 27cca23412 Fix lockdown message not being serialized properly (#37) 2023-10-02 19:18:07 +02:00
Jones 2d2c576ad4 Implement MiniMessage support (#35)
* fix(bungee): use correct main class for the bungee plugin

* fix(bungee, bukkit): don't reload the plugin on enable

* feat: implement MiniMessage (adventure) support

* feat(all): recode ServerWrapper

* refactor: make SonarCommand platform independent

* Implement automatic Velocity version increment (#32)

* feat: add MiniMessage support to the Disconnect packet

* fix: only use the adventure-platform dependency in the respective modules

* feat: add velocity-plugin.json that automatically adapts the version and plugin description

* fix: only save the configuration when necessary (https://github.com/jonesdevelopment/sonar/issues/33)

* feat: implement MiniMessage legacy deserialization and modern color formatting support

* fix(velocity): use correct sendMessage() function
2023-10-02 18:13:55 +02:00
Michel Elkenwaat 0adf7b0b6a Revert "Bump netty from 4.1.98.Final to 4.1.99.Final (#31)"
This reverts commit c6ff9370c2.
2023-10-02 18:09:29 +02:00
dependabot[bot] c6ff9370c2 Bump netty from 4.1.98.Final to 4.1.99.Final (#31)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.98.Final to 4.1.99.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.98.Final...netty-4.1.99.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 17:27:14 +02:00
46 changed files with 887 additions and 1146 deletions

No files matched your search

+6 -1
View File
@@ -38,11 +38,16 @@ subprojects {
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly("io.netty:netty-all:4.1.98.Final") // netty
// MiniMessage
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation("net.kyori:adventure-text-serializer-gson:4.14.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.14.0")
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.99.Final") // netty
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
group=xyz.jonesdev.sonar
version=2.0.1
version=2.0.2
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -54,6 +54,11 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
/**
* Set a custom verbose handler
*/
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Reloads the entire plugin
*/
@@ -24,7 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@Getter
@RequiredArgsConstructor
public final class CommandInvocation {
private final InvocationSender sender;
private final InvocationSource sender;
private final Subcommand command;
private final String[] rawArguments;
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 Sonar 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
/**
* Sends an empty chat message to the command executor
*/
public final void sendMessage() {
sendMessage(Component.empty());
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final String legacy) {
sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Sonar 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import java.util.*;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
default void cacheHelpMessage() {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
Component component = Component.textOfChildren(
Component.text(" ▪ ", NamedTextColor.GRAY),
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
Component.text(" - ", NamedTextColor.GRAY),
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
);
component = component.clickEvent(
net.kyori.adventure.text.event.ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
net.kyori.adventure.text.event.HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
))
);
CACHED_HELP_MESSAGE.add(component);
});
}
}
@@ -21,7 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
@@ -49,11 +49,9 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
protected final void incorrectUsage(final @NotNull InvocationSender sender) {
sender.sendMessage(
SONAR.getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", info.name() + " (" + arguments + ")")
);
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
}
public abstract void execute(final @NotNull CommandInvocation invocation);
@@ -20,13 +20,10 @@ package xyz.jonesdev.sonar.api.config;
import lombok.Getter;
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.exceptions.InvalidConfigurationException;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
@@ -66,44 +63,20 @@ public final class SimpleYamlConfig {
}
}
public void load() {
String backupContent = null;
try {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
try {
// Load all lines from the existing files before trying to load/overwrite it
backupContent = String.join(LINE_SEPARATOR, Files.readAllLines(file.toPath()));
} catch (IOException exception) {
Sonar.get().getLogger().error("Error while creating backup configuration: {}", exception);
}
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
} catch (InvalidConfigurationException exception) {
Sonar.get().getLogger().error("Invalid configuration: {}", exception);
// Try to save a backup configuration to prevent data loss
if (backupContent != null) {
final File backupFile = new File(file.getParentFile(), file.getName() + ".broken");
try (final FileWriter fileWriter = new FileWriter(backupFile, false)) {
fileWriter.write(backupContent);
Sonar.get().getLogger().info("Saved backup configuration to {}", backupFile.getName());
} catch (Throwable throwable) {
Sonar.get().getLogger().warn("Could not load from backup configuration: {}", throwable);
}
}
} catch (IOException exception) {
Sonar.get().getLogger().error("Error while loading configuration: {}", exception);
public void load() throws Exception {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
}
public void set(final String path, final Object v) {
File diff suppressed because it is too large. Load diff
@@ -48,10 +48,8 @@ public final class VerifiedPlayerController {
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration config = Sonar.get().getConfig();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = config.DATABASE_TYPE;
cachedDatabaseType = Sonar.get().getConfig().getDatabaseType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
@@ -38,8 +38,8 @@ public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final URL[] urls = new URL[config.DATABASE_TYPE.getDependencies().length];
for (final Dependency dependency : config.DATABASE_TYPE.getDependencies()) {
final URL[] urls = new URL[config.getDatabaseType().getDependencies().length];
for (final Dependency dependency : config.getDatabaseType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
@@ -50,9 +50,9 @@ public class DependencyLoader {
urls[dependency.ordinal()] = url;
}
final String type = config.DATABASE_TYPE.name().toLowerCase();
final String type = config.getDatabaseType().name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.SQL_URL + ":" + config.SQL_PORT + "/" + config.SQL_DATABASE;
"jdbc:" + type + "://" + config.getSqlUrl() + ":" + config.getSqlPort() + "/" + config.getSqlDatabase();
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
@@ -64,17 +64,17 @@ public class DependencyLoader {
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration configuration) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(configuration.DATABASE_TYPE.getDriverClassName());
final Class<?> driverClass = classLoader.loadClass(configuration.getDatabaseType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
if (!configuration.SQL_USER.isEmpty()) {
properties.put("user", configuration.SQL_USER);
if (!configuration.getSqlUser().isEmpty()) {
properties.put("user", configuration.getSqlUser());
}
if (!configuration.SQL_PASSWORD.isEmpty()) {
properties.put("password", configuration.SQL_PASSWORD);
if (!configuration.getSqlPassword().isEmpty()) {
properties.put("password", configuration.getSqlPassword());
}
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
@@ -65,7 +65,7 @@ public final class Fallback {
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK
|| queue.getQueuedPlayers().size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK;
return connected.size() > SONAR.getConfig().getMinimumPlayersForAttack()
|| queue.getQueuedPlayers().size() > SONAR.getConfig().getMinimumPlayersForAttack();
}
}
@@ -50,7 +50,7 @@ public final class FallbackQueue {
}
public void poll() {
final int max = Sonar.get().getConfig().MAXIMUM_QUEUE_POLLS;
final int max = Sonar.get().getConfig().getMaximumQueuePolls();
int index = 0;
// We need to be very careful here since we don't want any concurrency issues.
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
@@ -51,9 +52,20 @@ public interface FallbackUser<X, Y> {
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
void disconnect(final @NotNull String reason);
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
/**
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message component
*/
void disconnect(final @NotNull Component reason);
/**
* Sends a packet/message to the player
@@ -77,10 +89,10 @@ public interface FallbackUser<X, Y> {
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
disconnect(Sonar.get().getConfig().VERIFICATION_FAILED);
disconnect(Sonar.get().getConfig().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_FAILED_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
@@ -98,7 +110,7 @@ public interface FallbackUser<X, Y> {
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_BLACKLIST_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
@@ -17,22 +17,26 @@
package xyz.jonesdev.sonar.api.server;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import java.util.Optional;
public interface ServerWrapper {
@Getter
@RequiredArgsConstructor
public abstract class ServerWrapper {
/**
* @return Platform of the server (Velocity, BungeeCord, Spigot)
* Platform of the server (Velocity, BungeeCord, Spigot)
*/
SonarPlatform getPlatform();
private final SonarPlatform platform;
/**
* @param username Username of the player
* @return Optional player wrapped as InvocationSender
* @see xyz.jonesdev.sonar.api.command.InvocationSender
* @see InvocationSource
*/
Optional<InvocationSender> getOnlinePlayer(final String username);
public abstract Optional<InvocationSource> getOnlinePlayer(final String username);
}
@@ -57,8 +57,8 @@ public abstract class Verbose implements JVMProfiler {
broadcast(prepareActionBarFormat());
}
protected final @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().ACTION_BAR_LAYOUT
protected @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().getActionBarLayout()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
@@ -82,33 +82,35 @@ public abstract class Verbose implements JVMProfiler {
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().ANIMATION;
val animations = Sonar.get().getConfig().getAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
// Run action bar verbose
protected abstract void broadcast(final String message);
protected void broadcast(final String message) {
// This should be replaced by the wrapper
}
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
*/
public boolean isSubscribed(final @NotNull String subscriber) {
public final boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
}
/**
* @param subscriber Name of the player to subscribe
*/
public void subscribe(final @NotNull String subscriber) {
public final void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
}
/**
* @param subscriber Name of the player to unsubscribe
*/
public void unsubscribe(final @NotNull String subscriber) {
public final void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
}
}
+4 -1
View File
@@ -17,7 +17,7 @@ bukkit {
commands {
register("sonar") {
permission = "sonar.command"
// No permission check here
}
}
}
@@ -31,6 +31,9 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
@@ -21,11 +21,12 @@ import lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bukkit.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.command.BukkitInvocationSource;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
@@ -72,44 +73,25 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getOnlinePlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(message);
}
});
.map(BukkitInvocationSource::new);
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new SonarCommand());
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
@@ -15,17 +15,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.bukkit.command;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import java.util.*;
public final class BukkitInvocationSource extends InvocationSource {
private static final BukkitAudiences AUDIENCES = BukkitAudiences.create(SonarBukkit.INSTANCE.getPlugin());
public interface SonarBaseCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
List<Object> CACHED_HELP_MESSAGE = new Vector<>();
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -17,18 +17,14 @@
package xyz.jonesdev.sonar.bukkit.command;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -39,62 +35,9 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBaseCommand {
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
{
cacheHelpMessage();
}
@Override
@@ -103,6 +46,11 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
final String label,
final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -112,14 +60,14 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().COMMAND_COOL_DOWN);
sender.sendMessage(Sonar.get().getConfig().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
return false;
@@ -130,18 +78,7 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(message);
}
};
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
@@ -156,10 +93,8 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
invocationSource.sendMessage(Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
@@ -167,24 +102,22 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -197,12 +130,8 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
if (sender instanceof Player) {
((Player) sender).spigot().sendMessage((TextComponent) component);
} else {
sender.sendMessage(((TextComponent) component).getText());
}
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
return false;
@@ -213,6 +142,10 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
final Command command,
final String commandAlias,
final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
+4 -1
View File
@@ -7,7 +7,7 @@ apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
}
@@ -24,6 +24,9 @@ dependencies {
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
}
@@ -18,15 +18,15 @@
package xyz.jonesdev.sonar.bungee;
import lombok.Getter;
import net.md_5.bungee.api.chat.TextComponent;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bungee.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.command.BungeeInvocationSource;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
@@ -76,44 +76,25 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUNGEE) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.BUNGEE;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getPlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(new TextComponent(message));
}
});
.map(BungeeInvocationSource::new);
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new SonarCommand());
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
@@ -136,8 +117,5 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Sonar 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
private static final BungeeAudiences AUDIENCES = BungeeAudiences.create(SonarBungee.INSTANCE.getPlugin());
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -17,11 +17,8 @@
package xyz.jonesdev.sonar.bungee.command;
import net.md_5.bungee.api.ChatColor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
@@ -30,8 +27,8 @@ import net.md_5.bungee.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -42,72 +39,21 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand extends Command implements TabExecutor, SonarBaseCommand {
public SonarCommand() {
super("sonar", "sonar.command");
}
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
super("sonar");
cacheHelpMessage();
}
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getNoPermission()));
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -117,16 +63,14 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().COMMAND_COOL_DOWN));
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDown()));
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
));
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
return;
}
@@ -135,18 +79,7 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(new TextComponent(message));
}
};
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
@@ -161,8 +94,8 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
return;
@@ -172,24 +105,22 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -202,14 +133,18 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
sender.sendMessage((TextComponent) component);
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
}
@Override
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
@@ -21,8 +21,11 @@ import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.util.Natives;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.compress.PacketCompressor;
@@ -30,17 +33,18 @@ import net.md_5.bungee.compress.PacketDecompressor;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackListener.CachedMessages.LOCKDOWN_DISCONNECT;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
@@ -57,36 +61,35 @@ public final class FallbackListener implements Listener {
}
}
public static class CachedMessages {
static TextComponent LOCKDOWN_DISCONNECT;
public static void update() {
LOCKDOWN_DISCONNECT = new TextComponent(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
}
}
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
event.getPlayer().disconnect(LOCKDOWN_DISCONNECT);
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdownDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
}
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion()))
);
if (Sonar.get().getConfig().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
}
return;
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
event.getPlayer().sendMessage(
new TextComponent(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
);
} else if (Sonar.get().getConfig().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdownNotification()));
}
}
@@ -22,7 +22,7 @@ import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
@@ -45,8 +45,8 @@ public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, F
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull String reason) {
connection.closeWith(Disconnect.create(TextComponent.toLegacyText(new TextComponent(reason))));
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
@@ -27,13 +27,10 @@ import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
@@ -42,8 +39,7 @@ import static net.md_5.bungee.netty.PipelineUtils.*;
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
// cached object of our configuration to instantly access it
private static final SonarConfiguration CONFIG = Objects.requireNonNull(Sonar.get().getConfig());
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
private static final WriteBufferWaterMark MARK;
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
@@ -75,7 +71,7 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(MARK);
channel.pipeline().addLast(FRAME_DECODER, CONFIG.REPLACE_VAR_INT_DECODER
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.boot;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
@@ -41,6 +42,7 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
@@ -88,6 +90,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
} catch (Throwable throwable) {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace();
}
}
@@ -99,7 +102,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (getVerifiedPlayerController() != null
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().DATABASE_TYPE) {
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabaseType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
@@ -109,7 +112,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().VERIFICATION_DELAY, TimeUnit.MILLISECONDS, 250L
getConfig().getVerificationDelay(), TimeUnit.MILLISECONDS, 250L
);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
@@ -127,7 +127,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().VALID_LOCALE_REGEX;
final Pattern pattern = Sonar.get().getConfig().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
@@ -140,9 +140,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) return true;
// No need to check for empty or too long client brands since
// ProtocolUtil#readString already does exactly that.
final String read = ProtocolUtil.readString(content, Sonar.get().getConfig().MAXIMUM_BRAND_LENGTH);
final String read = ProtocolUtil.readString(content, Sonar.get().getConfig().getMaximumBrandLength());
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().VALID_BRAND_REGEX;
final Pattern pattern = Sonar.get().getConfig().getValidBrandRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
@@ -156,12 +156,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
@Override
public void handle(final @NotNull FallbackPacket packet) {
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().MAXIMUM_LOGIN_PACKETS
+ Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
final int maxPackets = Sonar.get().getConfig().getMaximumLoginPackets()
+ Sonar.get().getConfig().getMaximumMovementTicks();
checkFrame(++receivedPackets < maxPackets, "too many packets");
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().VERIFICATION_TIMEOUT;
final long timeout = Sonar.get().getConfig().getVerificationTimeout();
// Check if the time limit has exceeded
if (login.elapsed(timeout)) {
user.getChannel().close();
@@ -231,7 +231,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().CHECK_GRAVITY) {
if (!Sonar.get().getConfig().isCheckGravity()) {
finish();
return;
}
@@ -310,7 +310,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (deltaY > 0.07) {
// Verify the player if they sent correct movement packets
if (movementTick++ >= MAX_MOVEMENT_TICK) {
if (Sonar.get().getConfig().CHECK_COLLISIONS) {
if (Sonar.get().getConfig().isCheckCollisions()) {
if (state != State.COLLISIONS) {
// Set the state to COLLISIONS to avoid false positives
// and go on with the flow of the verification.
@@ -366,21 +366,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
user.getFallback().getConnected().remove(username);
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(
user.getInetAddress().toString(), uuid, login.getStart()
);
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), uuid, login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, uuid, user, login.delay()));
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().VERIFICATION_SUCCESS);
user.disconnect(Sonar.get().getConfig().getVerificationSuccess());
user.getFallback().getLogger().info(
Sonar.get().getConfig().VERIFICATION_SUCCESSFUL_LOG
.replace("%name%", username)
.replace("%time%", login.toString())
);
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerificationSuccessfulLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
}
@@ -99,7 +99,7 @@ public class FallbackPreparer {
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
MAX_MOVEMENT_TICK = Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
MAX_MOVEMENT_TICK = Sonar.get().getConfig().getMaximumMovementTicks();
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
@@ -184,7 +184,7 @@ public class FallbackPreparer {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
return joinGame;
@@ -196,7 +196,7 @@ public class FallbackPreparer {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
joinGame.setDimensionInfo(DIMENSION);
@@ -22,6 +22,8 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -48,7 +50,7 @@ public final class Disconnect implements FallbackPacket {
writeString(byteBuf, Objects.requireNonNull(reason));
}
public static Disconnect create(final String serialized) {
return new Disconnect(serialized);
public static Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -44,15 +44,14 @@ public final class BlacklistCommand extends Subcommand {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
@@ -61,41 +60,40 @@ public final class BlacklistCommand extends Subcommand {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_DUPLICATE);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistDuplicate());
return;
}
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD_WARNING
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
@@ -104,21 +102,21 @@ public final class BlacklistCommand extends Subcommand {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistNotFound());
return;
}
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_REMOVE
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -127,21 +125,18 @@ public final class BlacklistCommand extends Subcommand {
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistEmpty());
return;
}
SONAR.getFallback().getBlacklisted().invalidateAll();
invocation.getSender().sendMessage(
SONAR.getConfig().BLACKLIST_CLEARED
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted))
);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
break;
}
@@ -30,14 +30,12 @@ public final class LockdownCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
SONAR.getConfig().LOCKDOWN_ENABLED = !SONAR.getConfig().LOCKDOWN_ENABLED;
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", SONAR.getConfig().LOCKDOWN_ENABLED);
final boolean newState = !SONAR.getConfig().isLockdownEnabled();
SONAR.getConfig().setLockdownEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
SONAR.getConfig().getGeneralConfig().save();
invocation.getSender().sendMessage(
SONAR.getConfig().LOCKDOWN_ENABLED
? SONAR.getConfig().LOCKDOWN_ACTIVATED
: SONAR.getConfig().LOCKDOWN_DEACTIVATED
);
invocation.getSender().sendMessage(newState
? SONAR.getConfig().getLockdownActivated() : SONAR.getConfig().getLockdownDeactivated());
}
}
@@ -21,6 +21,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@SubcommandInfo(
name = "reload",
@@ -30,15 +31,12 @@ public final class ReloadCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final long startTime = System.currentTimeMillis();
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().RELOADING);
invocation.getSender().sendMessage(SONAR.getConfig().getReloading());
SONAR.reload();
final long timeTaken = System.currentTimeMillis() - startTime;
invocation.getSender().sendMessage(
SONAR.getConfig().RELOADED
.replace("%taken%", String.valueOf(timeTaken))
);
invocation.getSender().sendMessage(SONAR.getConfig().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
}
}
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.common.subcommand.impl;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@@ -34,33 +34,29 @@ public final class VerboseCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
InvocationSender verboseSubscriber = invocation.getSender();
InvocationSource verboseSubscriber = invocation.getSender();
// Support for '/sonar verbose [username]'
if (invocation.getRawArguments().length >= 2) {
final Optional<InvocationSender> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
final Optional<InvocationSource> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
if (optional.isPresent()) verboseSubscriber = optional.get();
}
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_UNSUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseUnsubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_UNSUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseUnsubscribed());
return;
}
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_SUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseSubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_SUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
}
}
@@ -46,27 +46,26 @@ public final class VerifiedCommand extends Subcommand {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final String realInetAddress = "/" + rawInetAddress;
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
return;
}
invocation.getSender().sendMessage("§ePrevious UUIDs for " + inetAddress + ":");
invocation.getSender().sendMessage("§ePrevious UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
@@ -76,28 +75,27 @@ public final class VerifiedCommand extends Subcommand {
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final String realInetAddress = "/" + rawInetAddress;
if (LOCK.contains(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_BLOCKED);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedBlocked());
return;
}
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
return;
}
@@ -106,8 +104,8 @@ public final class VerifiedCommand extends Subcommand {
SONAR.getVerifiedPlayerController().remove(realInetAddress);
LOCK.remove(realInetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_REMOVE
.replace("%ip%", inetAddress));
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -115,19 +113,18 @@ public final class VerifiedCommand extends Subcommand {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
if (verified == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedEmpty());
return;
}
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_CLEARED
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
break;
}
+20 -3
View File
@@ -4,17 +4,34 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion") // Proxy module
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-api:$velocityVersion")
testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-velocity:3.0.2")
}
tasks {
processResources {
val props = mapOf(
"name" to rootProject.name,
"version" to rootProject.version.toString().split("-")[0],
"description" to "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)",
"url" to "https://jonesdev.xyz/discord/",
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
)
inputs.properties(props)
filesMatching("velocity-plugin.json") {
expand(props)
}
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -18,15 +18,15 @@
package xyz.jonesdev.sonar.velocity;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.command.SonarCommand;
import xyz.jonesdev.sonar.velocity.command.VelocityInvocationSource;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
import xyz.jonesdev.sonar.velocity.verbose.VerboseWrapper;
@@ -72,30 +72,14 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.VELOCITY) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getAllPlayers().stream()
.filter(player -> player.getUsername().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getUsername();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(Component.text(message));
}
});
.map(VelocityInvocationSource::new);
}
};
@@ -106,7 +90,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
getPlugin().getMetricsFactory().make(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new SonarCommand());
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener(getFallback()));
@@ -129,8 +113,5 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -31,9 +31,10 @@ import org.slf4j.Logger;
import java.nio.file.Path;
@Getter
@Plugin(id = "sonar",
@Plugin(
id = "sonar",
name = "Sonar",
version = "2.0.1",
version = "$version",
authors = {"Jones Development", "Sonar Contributors"},
url = "https://jonesdev.xyz/discord/",
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
@@ -15,26 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.velocity.command;
public interface InvocationSender {
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
/**
* @return The name of the command executor
*/
String getName();
/**
* Sends a message to the command executor
*
* @param message Deserialized message
*/
void sendMessage(final String message);
/**
* Sends an empty chat message to the command executor
*/
default void sendMessage() {
sendMessage("");
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player
? ((Player) sender).getUsername()
: "Console", sender);
}
}
@@ -21,14 +21,11 @@ import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -39,54 +36,19 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
var component = Component.textOfChildren(
Component.text(" ▪ ", NamedTextColor.GRAY),
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
Component.text(" - ", NamedTextColor.GRAY),
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
);
component = component.clickEvent(
ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
))
);
CACHED_HELP_MESSAGE.add(component);
});
public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
{
cacheHelpMessage();
}
@Override
public void execute(final @NotNull Invocation invocation) {
if (!(invocation.source() instanceof ConsoleCommandSource)) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getNoPermission()));
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -96,16 +58,14 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().COMMAND_COOL_DOWN));
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommandCoolDown()));
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocation.source().sendMessage(Component.text(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
));
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
return;
}
@@ -114,20 +74,7 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return invocation.source() instanceof Player
? ((Player) invocation.source()).getUsername()
: "Console";
}
@Override
public void sendMessage(final String message) {
invocation.source().sendMessage(Component.text(message));
}
};
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
@@ -142,8 +89,8 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
if (!subcommand.get().getInfo().onlyConsole()
&& !invocation.source().hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
return;
@@ -153,25 +100,23 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(invocation.source() instanceof Player)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(invocation.source() instanceof ConsoleCommandSource)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, invocation.arguments());
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, invocation.arguments());
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1
&& sub.getInfo().argumentsRequired()) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -184,14 +129,18 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
invocation.source().sendMessage((Component) component);
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
}
@Override
public List<String> suggest(final @NotNull Invocation invocation) {
// Do not allow tab completion if the player does not have the required permission
if (!invocation.source().hasPermission("sonar.command")) {
return emptyList();
}
if (invocation.arguments().length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
@@ -221,9 +170,4 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
}
return emptyList();
}
@Override
public boolean hasPermission(final @NotNull Invocation invocation) {
return invocation.source().hasPermission("sonar.command");
}
}
@@ -55,34 +55,11 @@ import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
import static xyz.jonesdev.sonar.velocity.fallback.FallbackListener.CachedMessages.*;
@RequiredArgsConstructor
public final class FallbackListener {
private final @NotNull Fallback fallback;
public static class CachedMessages {
static Component LOCKDOWN_DISCONNECT;
static Component TOO_MANY_ONLINE_PER_IP;
static Component TOO_MANY_PLAYERS;
static Component BLACKLISTED;
static Component ALREADY_VERIFYING;
static Component ALREADY_QUEUED;
static Component TOO_FAST_RECONNECT;
public static Component INVALID_USERNAME;
public static void update() {
ALREADY_VERIFYING = Component.text(Sonar.get().getConfig().ALREADY_VERIFYING);
ALREADY_QUEUED = Component.text(Sonar.get().getConfig().ALREADY_QUEUED);
TOO_MANY_PLAYERS = Component.text(Sonar.get().getConfig().TOO_MANY_PLAYERS);
BLACKLISTED = Component.text(Sonar.get().getConfig().BLACKLISTED);
TOO_FAST_RECONNECT = Component.text(Sonar.get().getConfig().TOO_FAST_RECONNECT);
INVALID_USERNAME = Component.text(Sonar.get().getConfig().INVALID_USERNAME);
TOO_MANY_ONLINE_PER_IP = Component.text(Sonar.get().getConfig().TOO_MANY_ONLINE_PER_IP);
LOCKDOWN_DISCONNECT = Component.text(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
}
}
private static final DummyConnection CLOSED_MINECRAFT_CONNECTION;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
@@ -133,14 +110,14 @@ public final class FallbackListener {
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(mcConnection.getSessionHandler());
initialConnection.getConnection().closeWith(Disconnect.create(
BLACKLISTED,
Sonar.get().getConfig().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().ENABLE_VERIFICATION) return;
if (!Sonar.get().getConfig().isEnableVerification()) return;
// Check if the player is already verified.
// No one wants to be verified over and over again.
@@ -162,7 +139,7 @@ public final class FallbackListener {
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
ALREADY_VERIFYING,
Sonar.get().getConfig().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
@@ -170,9 +147,9 @@ public final class FallbackListener {
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().MAXIMUM_VERIFYING_PLAYERS) {
if (fallback.getConnected().size() > Sonar.get().getConfig().getMaximumVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_MANY_PLAYERS,
Sonar.get().getConfig().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
@@ -181,7 +158,7 @@ public final class FallbackListener {
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_FAST_RECONNECT,
Sonar.get().getConfig().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
@@ -190,7 +167,7 @@ public final class FallbackListener {
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
ALREADY_QUEUED,
Sonar.get().getConfig().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
@@ -214,9 +191,8 @@ public final class FallbackListener {
// Check if the username matches the valid name regex in order to prevent
// UTF-16 names or other types of flood attacks
if (!Sonar.get().getConfig().VALID_NAME_REGEX
.matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(INVALID_USERNAME, mcConnection.getProtocolVersion()));
if (!Sonar.get().getConfig().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getInvalidUsername(), mcConnection.getProtocolVersion()));
return;
}
@@ -225,10 +201,7 @@ public final class FallbackListener {
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(
Sonar.get().getConfig().VERIFICATION_READ_TIMEOUT,
TimeUnit.MILLISECONDS
)
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerificationReadTimeout(), TimeUnit.MILLISECONDS)
);
// We need to determine if the player is premium before we handle the connection,
@@ -250,27 +223,22 @@ public final class FallbackListener {
public void handle(final @NotNull LoginEvent event) {
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
LOCKDOWN_DISCONNECT, connectedPlayer.getProtocolVersion()
));
Sonar.get().getConfig().getLockdownDisconnect(), connectedPlayer.getProtocolVersion()));
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol()))
);
if (Sonar.get().getConfig().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
}
return;
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
event.getPlayer().sendMessage(
Component.text(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
);
} else if (Sonar.get().getConfig().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(Component.text(Sonar.get().getConfig().getLockdownNotification()));
}
}
@@ -278,7 +246,7 @@ public final class FallbackListener {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int maxOnlinePerIp = Sonar.get().getConfig().MAXIMUM_ONLINE_PER_IP;
final int maxOnlinePerIp = Sonar.get().getConfig().getMaximumOnlinePerIp();
if (maxOnlinePerIp > 0) {
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
@@ -288,7 +256,7 @@ public final class FallbackListener {
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
TOO_MANY_ONLINE_PER_IP, connectedPlayer.getProtocolVersion()
Sonar.get().getConfig().getTooManyOnlinePerIp(), connectedPlayer.getProtocolVersion()
));
}
}
@@ -122,11 +122,11 @@ public final class FallbackSessionHandler implements MinecraftSessionHandler {
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().LOG_CONNECTIONS) {
if (Sonar.get().getConfig().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().LOG_DURING_ATTACK) {
fallback.getLogger().info(Sonar.get().getConfig().VERIFICATION_CONNECT_LOG
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerificationConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.EqualsAndHashCode;
@@ -49,10 +48,8 @@ public final class FallbackUserWrapper implements FallbackUser<ConnectedPlayer,
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull String reason) {
final String serialized = ProtocolUtils.getJsonChatSerializer(connection.getProtocolVersion())
.serialize(Component.text(reason));
connection.closeWith(Disconnect.create(serialized));
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
@@ -0,0 +1,13 @@
{
"id": "sonar",
"name": "$name",
"version": "$version",
"description": "$description",
"url": "$url",
"authors": [
"Jones Development",
"Sonar Contributors"
],
"dependencies": [],
"main": "$main"
}