Skip to content
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 @@ -5,7 +5,7 @@
import nl.hauntedmc.proxyfeatures.api.io.config.ConfigMap;
import nl.hauntedmc.proxyfeatures.api.io.localization.MessageMap;
import nl.hauntedmc.proxyfeatures.features.VelocityBaseFeature;
import nl.hauntedmc.proxyfeatures.features.friends.command.FriendCommand;
import nl.hauntedmc.proxyfeatures.features.friends.command.AsyncFriendCommand;
import nl.hauntedmc.proxyfeatures.features.friends.entity.FriendRelationEntity;
import nl.hauntedmc.proxyfeatures.features.friends.entity.FriendSettingsEntity;
import nl.hauntedmc.proxyfeatures.features.friends.entity.FriendsService;
Expand Down Expand Up @@ -118,7 +118,7 @@ public void initialize() {
service = new FriendsService(this, cache);

getLifecycleManager().getCommandManager()
.registerFeatureCommand(new FriendCommand(this));
.registerFeatureCommand(new AsyncFriendCommand(this));

getLifecycleManager().getListenerManager().registerListener(new FriendActivityListener(this));
}
Expand All @@ -138,5 +138,4 @@ public FriendsService getService() {
public FriendsCache getCache() {
return cache;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package nl.hauntedmc.proxyfeatures.features.friends.command;

import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.Player;
import nl.hauntedmc.proxyfeatures.api.command.FeatureCommand;
import nl.hauntedmc.proxyfeatures.features.friends.Friends;

import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

/**
* Keeps database-backed friend command work away from Velocity's command and Netty threads.
*/
public final class AsyncFriendCommand implements FeatureCommand {

private static final Set<String> NO_ARGUMENT_SUBCOMMANDS = Set.of(
"acceptall", "denyall", "disable", "enable"
);
private static final Set<String> SINGLE_ARGUMENT_SUBCOMMANDS = Set.of(
"add", "remove", "accept", "deny", "cancel", "server", "block", "unblock"
);
private static final Set<String> OPTIONAL_PAGE_SUBCOMMANDS = Set.of("list", "requests");

private final Friends feature;
private final FriendCommand delegate;

public AsyncFriendCommand(Friends feature) {
this.feature = feature;
this.delegate = new FriendCommand(feature);
}

@Override
public void execute(Invocation invocation) {
CommandSource source = invocation.source();
if (!(source instanceof Player)) {
delegate.execute(invocation);
return;
}

String alias = invocation.alias();
String[] arguments = invocation.arguments().clone();
try {
feature.getLifecycleManager().getTaskManager().scheduleTask(() -> {
try {
delegate.execute(capturedInvocation(source, alias, arguments));
} catch (RuntimeException exception) {
logFailure("Command execution", exception);
sendDataUnavailable(source);
}
});
} catch (RuntimeException exception) {
logFailure("Command scheduling", exception);
sendDataUnavailable(source);
}
}

@Override
public boolean hasPermission(Invocation invocation) {
return delegate.hasPermission(invocation);
}

@Override
public CompletableFuture<List<String>> suggestAsync(Invocation invocation) {
CommandSource source = invocation.source();
if (!(source instanceof Player)) {
return CompletableFuture.completedFuture(List.of());
}

String[] arguments = invocation.arguments().clone();
if (hasNoFurtherArguments(arguments)) {
return CompletableFuture.completedFuture(List.of());
}

// Pure subcommand filtering is safe synchronously. Numeric first arguments query friend data
// for page counts and therefore must use the background path as well.
if (arguments.length == 0
|| (arguments.length == 1 && !isPositiveInteger(arguments[0]))) {
try {
CompletableFuture<List<String>> result = delegate.suggestAsync(invocation);
return result == null ? CompletableFuture.completedFuture(List.of()) : result;
} catch (RuntimeException exception) {
logFailure("Tab completion", exception);
return CompletableFuture.completedFuture(List.of());
}
}

String alias = invocation.alias();
CompletableFuture<List<String>> result = new CompletableFuture<>();
try {
feature.getLifecycleManager().getTaskManager().scheduleTask(() -> {
try {
CompletableFuture<List<String>> delegated =
delegate.suggestAsync(capturedInvocation(source, alias, arguments));
if (delegated == null) {
result.complete(List.of());
return;
}
delegated.whenComplete((suggestions, failure) -> {
if (failure != null) {
logFailure("Tab completion", failure);
result.complete(List.of());
} else {
result.complete(suggestions == null ? List.of() : suggestions);
}
});
} catch (RuntimeException exception) {
logFailure("Tab completion", exception);
result.complete(List.of());
}
});
} catch (RuntimeException exception) {
logFailure("Tab-completion scheduling", exception);
result.complete(List.of());
}
return result;
}

static boolean hasNoFurtherArguments(String[] arguments) {
if (arguments.length == 0) {
return false;
}

String subcommand = arguments[0].toLowerCase(Locale.ROOT);
if (NO_ARGUMENT_SUBCOMMANDS.contains(subcommand)) {
return arguments.length >= 2;
}
if (SINGLE_ARGUMENT_SUBCOMMANDS.contains(subcommand)) {
return arguments.length > 2;
}
if (OPTIONAL_PAGE_SUBCOMMANDS.contains(subcommand)) {
return arguments.length > 2;
}
return false;
}

private static boolean isPositiveInteger(String value) {
if (value == null || value.isEmpty()) {
return false;
}
for (int i = 0; i < value.length(); i++) {
if (!Character.isDigit(value.charAt(i))) {
return false;
}
}
return true;
}

private static SimpleCommand.Invocation capturedInvocation(
CommandSource source,
String alias,
String[] arguments
) {
return new SimpleCommand.Invocation() {
@Override
public CommandSource source() {
return source;
}

@Override
public String[] arguments() {
return arguments;
}

@Override
public String alias() {
return alias;
}
};
}

private void sendDataUnavailable(CommandSource source) {
source.sendMessage(feature.getLocalizationHandler()
.getMessage("friend.data_unavailable")
.forAudience(source)
.build());
}

private void logFailure(String operation, Throwable failure) {
String message = failure == null || failure.getMessage() == null
? failure == null ? "unknown" : failure.getClass().getSimpleName()
: failure.getMessage();
feature.getLogger().error("[Friends] " + operation + " failed: " + message);
}

@Override
public String getName() {
return delegate.getName();
}

@Override
public String[] getAliases() {
return delegate.getAliases();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import nl.hauntedmc.proxyfeatures.api.command.FeatureCommand;
import nl.hauntedmc.proxyfeatures.api.util.text.placeholder.MessagePlaceholders;
import nl.hauntedmc.proxyfeatures.features.sanctions.Sanctions;
import nl.hauntedmc.proxyfeatures.features.sanctions.entity.SanctionType;
import nl.hauntedmc.proxyfeatures.features.sanctions.service.SanctionSuggestionLookup;

import java.util.List;
import java.util.Map;
Expand All @@ -19,7 +21,6 @@ public UnbanCommand(Sanctions feature) {
this.feature = feature;
}


public void execute(Invocation inv) {
CommandSource src = inv.source();
String[] a = inv.arguments();
Expand Down Expand Up @@ -52,21 +53,13 @@ public void execute(Invocation inv) {
}

String actorName = (src instanceof Player pl) ? pl.getUsername() : "CONSOLE";

// Feedback to executor
sendMsg(src, "sanctions.unbanned", Map.of("target", target.getUsername()));

// Broadcast to staff
Map<String, String> ph = Map.of("target", target.getUsername(), "actor", actorName);
feature.getService().broadcastToStaff(
"sanctions.announce.unban",
MessagePlaceholders.of(ph)
);

feature.getService().broadcastToStaff("sanctions.announce.unban", MessagePlaceholders.of(ph));
feature.getDiscordService().sendUnban(target, actorName);
}


public boolean hasPermission(Invocation inv) {
return inv.source().hasPermission("proxyfeatures.feature.sanctions.command.unban");
}
Expand All @@ -79,16 +72,37 @@ public String[] getAliases() {
return new String[0];
}


public CompletableFuture<List<String>> suggestAsync(Invocation invocation) {
String[] a = invocation.arguments();
String prefix = (a.length >= 1) ? a[0] : "";
List<String> names = feature.getService()
.suggestActiveTargetNames(nl.hauntedmc.proxyfeatures.features.sanctions.entity.SanctionType.BAN, prefix, 20);
return CompletableFuture.completedFuture(names);
String[] arguments = invocation.arguments();
if (arguments.length > 1) {
return CompletableFuture.completedFuture(List.of());
}

String prefix = arguments.length == 1 ? arguments[0] : "";
CompletableFuture<List<String>> suggestions = new CompletableFuture<>();
try {
feature.getLifecycleManager().getTaskManager().scheduleTask(() -> {
try {
suggestions.complete(SanctionSuggestionLookup.activeTargetNames(feature, SanctionType.BAN, prefix, 20));
} catch (RuntimeException exception) {
logSuggestionFailure(exception);
suggestions.complete(List.of());
}
});
} catch (RuntimeException exception) {
logSuggestionFailure(exception);
suggestions.complete(List.of());
}
return suggestions;
}

private void logSuggestionFailure(Throwable failure) {
String message = failure == null || failure.getMessage() == null
? failure == null ? "unknown" : failure.getClass().getSimpleName()
: failure.getMessage();
feature.getLogger().error("[Sanctions] Failed to provide /unban suggestions: " + message);
}

// helpers
private void sendMsg(CommandSource src, String key) {
src.sendMessage(feature.getLocalizationHandler().getMessage(key).forAudience(src).build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public UnbanIpCommand(Sanctions feature) {
this.feature = feature;
}


public void execute(Invocation inv) {
CommandSource src = inv.source();
String[] a = inv.arguments();
Expand Down Expand Up @@ -49,11 +48,9 @@ public void execute(Invocation inv) {
return;
}

// Feedback to executor
sendMsg(src, "sanctions.unbanned_ip", Map.of("ip", ip));
}


public boolean hasPermission(Invocation inv) {
return inv.source().hasPermission("proxyfeatures.feature.sanctions.command.unbanip");
}
Expand All @@ -66,15 +63,37 @@ public String[] getAliases() {
return new String[0];
}


public CompletableFuture<List<String>> suggestAsync(Invocation invocation) {
String[] a = invocation.arguments();
String prefix = (a.length >= 1) ? a[0] : "";
List<String> ips = feature.getService().suggestActiveBannedIps(prefix, 20);
return CompletableFuture.completedFuture(ips);
String[] arguments = invocation.arguments();
if (arguments.length > 1) {
return CompletableFuture.completedFuture(List.of());
}

String prefix = arguments.length == 1 ? arguments[0] : "";
CompletableFuture<List<String>> suggestions = new CompletableFuture<>();
try {
feature.getLifecycleManager().getTaskManager().scheduleTask(() -> {
try {
suggestions.complete(feature.getService().suggestActiveBannedIps(prefix, 20));
} catch (RuntimeException exception) {
logSuggestionFailure(exception);
suggestions.complete(List.of());
}
});
} catch (RuntimeException exception) {
logSuggestionFailure(exception);
suggestions.complete(List.of());
}
return suggestions;
}

private void logSuggestionFailure(Throwable failure) {
String message = failure == null || failure.getMessage() == null
? failure == null ? "unknown" : failure.getClass().getSimpleName()
: failure.getMessage();
feature.getLogger().error("[Sanctions] Failed to provide /unbanip suggestions: " + message);
}

// helpers
private String normalizeIp(String ip) {
try {
return InetAddress.getByName(ip).getHostAddress();
Expand Down
Loading