Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recode command and subcommand execution logic #94

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;

import java.util.function.Predicate;

@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
private final boolean player;
private final Predicate<String> permissionFunction;

/**
* Sends an empty chat message to the command executor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,65 @@ public interface SonarCommand {

List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();

default void handle(final @NotNull InvocationSource source, final String[] args) {
if (source.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!source.getPermissionFunction().test("sonar.command")) {
source.sendMessage(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
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);

// There were some exploits with spamming commands in the past.
// 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) {
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());

// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;

source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}

DELAY.put(source);
}

if (args.length > 0) {
// Search subcommand if command arguments are present
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();

// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
subcommand.get().invoke(source, args);
return;
}
}

// 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 Component component : CACHED_HELP_MESSAGE) {
source.sendMessage(component);
}
}

static void prepareCachedMessages() {
// Cache help message
CACHED_HELP_MESSAGE.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@

public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender), sender instanceof Player);
super(sender.getName(),
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,15 @@

package xyz.jonesdev.sonar.bukkit.command;

import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static java.util.Collections.emptyList;

Expand All @@ -43,70 +38,9 @@ public boolean onCommand(final CommandSender sender,
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);

if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.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
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);

// There were some exploits with spamming commands in the past.
// 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) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());

// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;

invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}

DELAY.put(sender);
}

Optional<Subcommand> subcommand = Optional.empty();

if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();

// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
}

if (!subcommand.isPresent()) {

// 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 Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, args);
}
return false;
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@

public final class BungeeInvocationSource extends InvocationSource {
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), SonarBungee.INSTANCE.getBungeeAudiences().sender(sender), sender instanceof ProxiedPlayer);
super(sender.getName(),
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender instanceof ProxiedPlayer,
sender::hasPermission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,12 @@

package xyz.jonesdev.sonar.bungee.command;

import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;

import java.util.Arrays;
import java.util.Optional;

import static java.util.Collections.emptyList;

Expand All @@ -42,69 +36,8 @@ public BungeeSonarCommand() {
public void execute(final @NotNull CommandSender sender, final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);

if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(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
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);

// There were some exploits with spamming commands in the past.
// 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) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());

// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;

invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}

DELAY.put(sender);
}

Optional<Subcommand> subcommand = Optional.empty();

if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();

// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
}

if (!subcommand.isPresent()) {

// 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 Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, args);
}
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console", sender, sender instanceof Player);
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
sender,
sender instanceof Player,
sender::hasPermission);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,11 @@
package xyz.jonesdev.sonar.velocity.command;

import com.velocitypowered.api.command.SimpleCommand;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static java.util.Collections.emptyList;

Expand All @@ -37,69 +32,8 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
public void execute(final @NotNull Invocation invocation) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());

if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocationSource.sendMessage(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
final long mapTimestamp = DELAY.asMap().getOrDefault(invocation.source(), -1L);

// There were some exploits with spamming commands in the past.
// 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) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());

// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;

invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}

DELAY.put(invocation.source());
}

Optional<Subcommand> subcommand = Optional.empty();

if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(invocation.arguments()[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(invocation.arguments()[0])))
.findFirst();

// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !invocation.source().hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
}

if (subcommand.isEmpty()) {

// 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 Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, invocation.arguments());
}
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, invocation.arguments());
}

@Override
Expand Down