Add console command execution and user/role limiting to custom commands

This commit is contained in:
Vankka 2024-11-16 23:26:29 +02:00
parent 227138fc94
commit 791abc7888
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
12 changed files with 192 additions and 83 deletions

View File

@ -33,8 +33,7 @@ import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public abstract class AbstractInteractionEvent<T extends GenericInteractionCreateEvent> extends public abstract class AbstractInteractionEvent<T extends GenericInteractionCreateEvent> extends AbstractDiscordEvent<T> {
AbstractDiscordEvent<T> {
protected final ComponentIdentifier identifier; protected final ComponentIdentifier identifier;
protected final DiscordUser user; protected final DiscordUser user;

View File

@ -31,11 +31,11 @@ import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifi
import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent; import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public abstract class AbstractDeferrableInteractionEvent<T extends GenericInteractionCreateEvent> extends AbstractInteractionEvent<T> { public class AbstractInteractionWithHookEvent<T extends GenericInteractionCreateEvent> extends AbstractInteractionEvent<T> {
protected final DiscordInteractionHook hook; protected final DiscordInteractionHook hook;
public AbstractDeferrableInteractionEvent( public AbstractInteractionWithHookEvent(
T jdaEvent, T jdaEvent,
ComponentIdentifier identifier, ComponentIdentifier identifier,
DiscordUser user, DiscordUser user,

View File

@ -30,7 +30,7 @@ import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
public class DiscordModalInteractionEvent extends AbstractDeferrableInteractionEvent<ModalInteractionEvent> { public class DiscordModalInteractionEvent extends AbstractInteractionWithHookEvent<ModalInteractionEvent> {
public DiscordModalInteractionEvent( public DiscordModalInteractionEvent(
ModalInteractionEvent jdaEvent, ModalInteractionEvent jdaEvent,

View File

@ -32,7 +32,7 @@ import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook; import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.events.discord.interaction.AbstractDeferrableInteractionEvent; import com.discordsrv.api.events.discord.interaction.AbstractInteractionWithHookEvent;
import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -40,7 +40,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public abstract class AbstractCommandInteractionEvent<E extends GenericCommandInteractionEvent> public abstract class AbstractCommandInteractionEvent<E extends GenericCommandInteractionEvent>
extends AbstractDeferrableInteractionEvent<E> { extends AbstractInteractionWithHookEvent<E> {
private final DiscordSRVApi discordSRV; private final DiscordSRVApi discordSRV;
@ -63,6 +63,8 @@ public abstract class AbstractCommandInteractionEvent<E extends GenericCommandIn
return reply(message, false); return reply(message, false);
} }
public abstract CompletableFuture<DiscordInteractionHook> deferReply(boolean ephemeral);
@Nullable @Nullable
public String getOptionAsString(String name) { public String getOptionAsString(String name) {
OptionMapping mapping = jdaEvent.getOption(name); OptionMapping mapping = jdaEvent.getOption(name);

View File

@ -28,10 +28,10 @@ import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook; import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.events.discord.interaction.AbstractDeferrableInteractionEvent; import com.discordsrv.api.events.discord.interaction.AbstractInteractionWithHookEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
public class DiscordButtonInteractionEvent extends AbstractDeferrableInteractionEvent<ButtonInteractionEvent> { public class DiscordButtonInteractionEvent extends AbstractInteractionWithHookEvent<ButtonInteractionEvent> {
public DiscordButtonInteractionEvent( public DiscordButtonInteractionEvent(
ButtonInteractionEvent jdaEvent, ButtonInteractionEvent jdaEvent,
@ -39,8 +39,8 @@ public class DiscordButtonInteractionEvent extends AbstractDeferrableInteraction
DiscordUser user, DiscordUser user,
DiscordGuildMember member, DiscordGuildMember member,
DiscordMessageChannel channel, DiscordMessageChannel channel,
DiscordInteractionHook interaction DiscordInteractionHook hook
) { ) {
super(jdaEvent, identifier, user, member, channel, interaction); super(jdaEvent, identifier, user, member, channel, hook);
} }
} }

View File

@ -28,10 +28,10 @@ import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook; import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.events.discord.interaction.AbstractDeferrableInteractionEvent; import com.discordsrv.api.events.discord.interaction.AbstractInteractionWithHookEvent;
import net.dv8tion.jda.api.events.interaction.component.GenericSelectMenuInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.GenericSelectMenuInteractionEvent;
public class DiscordSelectMenuInteractionEvent extends AbstractDeferrableInteractionEvent<GenericSelectMenuInteractionEvent<?, ?>> { public class DiscordSelectMenuInteractionEvent extends AbstractInteractionWithHookEvent<GenericSelectMenuInteractionEvent<?, ?>> {
public DiscordSelectMenuInteractionEvent( public DiscordSelectMenuInteractionEvent(
GenericSelectMenuInteractionEvent<?, ?> jdaEvent, GenericSelectMenuInteractionEvent<?, ?> jdaEvent,

View File

@ -91,7 +91,7 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute; DiscordCommandConfig.ExecuteConfig config = discordSRV.config().discordCommand.execute;
boolean ephemeral = config.ephemeral; boolean ephemeral = config.ephemeral;
if (!config.enabled) { if (!config.enabled) {
event.reply(SendableDiscordMessage.builder().setContent("The execute command is disabled").build(), ephemeral); event.reply(SendableDiscordMessage.builder().setContent("The execute command is disabled").build(), true); // TODO: translation
return; return;
} }
@ -101,7 +101,7 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
} }
if (isNotAcceptableCommand(event.getMember(), event.getUser(), command, false)) { if (isNotAcceptableCommand(event.getMember(), event.getUser(), command, false)) {
event.reply(SendableDiscordMessage.builder().setContent("You do not have permission to run that command").build(), ephemeral); event.reply(SendableDiscordMessage.builder().setContent("You do not have permission to run that command").build(), true); // TODO: translation
return; return;
} }

View File

@ -21,9 +21,12 @@ package com.discordsrv.common.config.main;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption; import com.discordsrv.api.discord.entity.interaction.command.CommandOption;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed; import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.configurate.annotation.Constants;
import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
@ConfigSerializable @ConfigSerializable
@ -48,21 +51,52 @@ public class CustomCommandConfig {
return config; return config;
} }
@Comment("The command in Discord, this can be in up-to 3 parts (seperated by spaces).\n"
+ "You cannot specify commands on the 2nd and 3rd layer for the same main command at once.\n"
+ "You cannot specify a action for the main command if you specify something for the same main command on the 2nd or 3rd layer")
public String command = ""; public String command = "";
@Comment("The description of the command, will be shown to the user")
public String description = ""; public String description = "";
@Comment("If the command output should only be visible to the user who ran the command")
public boolean ephemeral = false; public boolean ephemeral = false;
public List<OptionConfig> options = new ArrayList<>(); public List<OptionConfig> options = new ArrayList<>();
@Comment("Only one of the constraints has to be true to allow execution")
public List<ConstraintConfig> constraints = new ArrayList<>(Collections.singletonList(new ConstraintConfig()));
@Comment("A list of console commands to run upon this commands execution")
public List<String> consoleCommandsToRun = new ArrayList<>();
public SendableDiscordMessage.Builder response = SendableDiscordMessage.builder().setContent("test"); public SendableDiscordMessage.Builder response = SendableDiscordMessage.builder().setContent("test");
@ConfigSerializable @ConfigSerializable
public static class OptionConfig { public static class OptionConfig {
@Comment("Acceptable options are: %1")
@Constants.Comment("STRING, LONG, DOUBLE, BOOLEAN, USER, CHANNEL, ROLE, MENTIONABLE, ATTACHMENT")
public CommandOption.Type type = CommandOption.Type.USER; public CommandOption.Type type = CommandOption.Type.USER;
@Comment("The name of this option, will be shown to the user")
public String name = "target_user"; public String name = "target_user";
@Comment("The description of this option, will be shown to the user")
public String description = "The user to greet"; public String description = "The user to greet";
@Comment("If this option is required to run the command")
public boolean required = true; public boolean required = true;
} }
@ConfigSerializable
public static class ConstraintConfig {
@Comment("The role and user ids that should/should not be allowed to run this custom command")
public List<Long> roleAndUserIds = new ArrayList<>();
@Comment("true for blacklisting the specified roles and users, false for whitelisting")
public boolean blacklist = true;
}
} }

View File

@ -54,4 +54,12 @@ public class DiscordChatInputInteractionEventImpl extends DiscordChatInputIntera
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih)) .thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
); );
} }
@Override
public CompletableFuture<DiscordInteractionHook> deferReply(boolean ephemeral) {
return discordSRV.discordAPI().mapExceptions(
() -> jdaEvent.deferReply(ephemeral).submit()
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
);
}
} }

View File

@ -42,7 +42,9 @@ public class DiscordMessageContextInteractionEventImpl extends DiscordMessageCon
ComponentIdentifier identifier, ComponentIdentifier identifier,
DiscordUser user, DiscordUser user,
DiscordGuildMember member, DiscordGuildMember member,
DiscordMessageChannel channel, DiscordInteractionHook interaction) { DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction); super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
this.discordSRV = discordSRV; this.discordSRV = discordSRV;
} }
@ -54,4 +56,12 @@ public class DiscordMessageContextInteractionEventImpl extends DiscordMessageCon
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih)) .thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
); );
} }
@Override
public CompletableFuture<DiscordInteractionHook> deferReply(boolean ephemeral) {
return discordSRV.discordAPI().mapExceptions(
() -> jdaEvent.deferReply(ephemeral).submit()
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
);
}
} }

View File

@ -54,4 +54,12 @@ public class DiscordUserContextInteractionEventImpl extends DiscordUserContextIn
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih)) .thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
); );
} }
@Override
public CompletableFuture<DiscordInteractionHook> deferReply(boolean ephemeral) {
return discordSRV.discordAPI().mapExceptions(
() -> jdaEvent.deferReply(ephemeral).submit()
.thenApply(ih -> new DiscordInteractionHookImpl(discordSRV, ih))
);
}
} }

View File

@ -21,11 +21,13 @@ package com.discordsrv.common.feature.customcommands;
import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser; import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordChannel; import com.discordsrv.api.discord.entity.channel.DiscordChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.guild.DiscordRole; import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption; import com.discordsrv.api.discord.entity.interaction.command.CommandOption;
import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand; import com.discordsrv.api.discord.entity.interaction.command.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.events.discord.interaction.command.AbstractCommandInteractionEvent;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.CustomCommandConfig; import com.discordsrv.common.config.main.CustomCommandConfig;
import com.discordsrv.common.core.logging.NamedLogger; import com.discordsrv.common.core.logging.NamedLogger;
@ -99,73 +101,8 @@ public class CustomCommandModule extends AbstractModule<DiscordSRV> {
); );
} }
commandBuilder.setEventHandler(event -> { ExecutionHandler handler = new ExecutionHandler(config);
SendableDiscordMessage.Formatter formatter = config.response.toFormatter(); commandBuilder.setEventHandler(handler::accept);
for (CustomCommandConfig.OptionConfig option : config.options) {
String optionName = option.name;
Object context;
String reLookup = null;
switch (option.type) {
case CHANNEL:
context = event.getOptionAsChannel(optionName);
reLookup = "channel";
break;
case USER:
context = event.getOptionAsUser(optionName);
reLookup = "user";
break;
case ROLE:
context = event.getOptionAsRole(optionName);
reLookup = "role";
break;
case MENTIONABLE:
Long id = event.getOptionAsLong(optionName);
if (id == null) {
context = event.getOptionAsString(optionName);
break;
}
DiscordUser user = discordSRV.discordAPI().getUserById(id);
if (user != null) {
context = user;
reLookup = "user";
break;
}
DiscordRole role = discordSRV.discordAPI().getRoleById(id);
if (role != null) {
context = role;
reLookup = "role";
break;
}
DiscordChannel channel = discordSRV.discordAPI().getChannelById(id);
if (channel != null) {
context = channel;
reLookup = "channel";
break;
}
context = event.getOptionAsString(optionName);
break;
default:
context = event.getOptionAsString(optionName);
break;
}
formatter = formatter.addPlaceholder("option_" + optionName, context, reLookup);
}
SendableDiscordMessage message = formatter.applyPlaceholderService().build();
event.reply(message, config.ephemeral).whenComplete((ih, t) -> {
if (t != null) {
logger().debug("Failed to reply to custom command: " + config.command, t);
}
});
});
DiscordCommand command = commandBuilder.build(); DiscordCommand command = commandBuilder.build();
LayerCommand foundLayer = layeredCommands.stream() LayerCommand foundLayer = layeredCommands.stream()
@ -230,4 +167,115 @@ public class CustomCommandModule extends AbstractModule<DiscordSRV> {
return commands; return commands;
} }
} }
public class ExecutionHandler implements Consumer<AbstractCommandInteractionEvent<?>> {
private final CustomCommandConfig config;
public ExecutionHandler(CustomCommandConfig config) {
this.config = config;
}
@Override
public void accept(AbstractCommandInteractionEvent<?> event) {
DiscordGuildMember member = event.getMember();
if (member == null) {
return;
}
boolean anyAllowingConstraint = config.constraints.isEmpty();
for (CustomCommandConfig.ConstraintConfig constraint : config.constraints) {
boolean included = constraint.roleAndUserIds.contains(member.getUser().getId());
if (!included) {
for (DiscordRole role : member.getRoles()) {
if (constraint.roleAndUserIds.contains(role.getId())) {
included = true;
break;
}
}
}
if (included != constraint.blacklist) {
anyAllowingConstraint = true;
break;
}
}
if (!anyAllowingConstraint) {
event.reply(SendableDiscordMessage.builder().setContent("You do not have permission to run that command").build(), true); // TODO: translation
return;
}
List<String> commandsToRun = config.consoleCommandsToRun;
for (String command : commandsToRun) {
discordSRV.console().runCommandWithLogging(discordSRV, event.getUser(), command);
}
SendableDiscordMessage.Formatter formatter = config.response.toFormatter();
optionsToFormatter(event, formatter);
SendableDiscordMessage message = formatter.applyPlaceholderService().build();
event.reply(message, config.ephemeral).whenComplete((__, t) -> {
if (t != null) {
logger().debug("Failed to reply to custom command: " + config.command, t);
}
});
}
private void optionsToFormatter(AbstractCommandInteractionEvent<?> event, SendableDiscordMessage.Formatter formatter) {
for (CustomCommandConfig.OptionConfig option : config.options) {
String optionName = option.name;
Object context;
String reLookup = null;
switch (option.type) {
case CHANNEL:
context = event.getOptionAsChannel(optionName);
reLookup = "channel";
break;
case USER:
context = event.getOptionAsUser(optionName);
reLookup = "user";
break;
case ROLE:
context = event.getOptionAsRole(optionName);
reLookup = "role";
break;
case MENTIONABLE:
Long id = event.getOptionAsLong(optionName);
if (id == null) {
context = event.getOptionAsString(optionName);
break;
}
DiscordUser user = discordSRV.discordAPI().getUserById(id);
if (user != null) {
context = user;
reLookup = "user";
break;
}
DiscordRole role = discordSRV.discordAPI().getRoleById(id);
if (role != null) {
context = role;
reLookup = "role";
break;
}
DiscordChannel channel = discordSRV.discordAPI().getChannelById(id);
if (channel != null) {
context = channel;
reLookup = "channel";
break;
}
context = event.getOptionAsString(optionName);
break;
default:
context = event.getOptionAsString(optionName);
break;
}
formatter = formatter.addPlaceholder("option_" + optionName, context, reLookup);
}
}
}
} }