Add custom commands

This commit is contained in:
Vankka 2024-10-13 16:19:07 +03:00
parent b8f8b7d585
commit 7d4755240f
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
21 changed files with 485 additions and 37 deletions

View File

@ -39,6 +39,13 @@ import java.util.concurrent.CompletableFuture;
*/
public interface DiscordAPI {
/**
* Gets a Discord channel by id, the provided entity should not be stored for long periods of time.
* @param id the id for the channel
* @return the channel
*/
DiscordChannel getChannelById(long id);
/**
* Gets a Discord message channel by id, the provided entity should not be stored for long periods of time.
* @param id the id for the message channel
@ -95,7 +102,6 @@ public interface DiscordAPI {
@Nullable
DiscordStageChannel getStageChannelById(long id);
/**
* Gets a Discord thread channel by id from the cache, the provided entity should not be stored for long periods of time.
* @param id the id for the thread channel

View File

@ -55,9 +55,9 @@ public class DiscordCommand implements JDAEntity<CommandData> {
* @see DiscordChatInputInteractionEvent
*/
public static ChatInputBuilder chatInput(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(CHAT_INPUT_NAME_REGEX) String name,
String description
@NotNull ComponentIdentifier id,
@NotNull @org.intellij.lang.annotations.Pattern(CHAT_INPUT_NAME_REGEX) String name,
@NotNull String description
) {
if (!CHAT_INPUT_NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException("Name must be alphanumeric (dashes allowed), 1 and 32 characters");
@ -74,8 +74,8 @@ public class DiscordCommand implements JDAEntity<CommandData> {
* @see DiscordUserContextInteractionEvent
*/
public static Builder<DiscordUserContextInteractionEvent> user(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(".{1,32}") String name
@NotNull ComponentIdentifier id,
@NotNull @org.intellij.lang.annotations.Pattern(".{1,32}") String name
) {
return new Builder<>(id, CommandType.USER, name);
}
@ -89,8 +89,8 @@ public class DiscordCommand implements JDAEntity<CommandData> {
* @see DiscordMessageContextInteractionEvent
*/
public static Builder<DiscordMessageContextInteractionEvent> message(
ComponentIdentifier id,
@org.intellij.lang.annotations.Pattern(".{1,32}") String name
@NotNull ComponentIdentifier id,
@NotNull @org.intellij.lang.annotations.Pattern(".{1,32}") String name
) {
return new Builder<>(id, CommandType.MESSAGE, name);
}

View File

@ -373,10 +373,18 @@ public interface SendableDiscordMessage {
@NotNull
Formatter addContext(Object... context);
default Formatter addPlaceholder(String placeholder, Object replacement, String reLookup) {
return addContext(new SinglePlaceholder(placeholder, replacement, reLookup));
}
default Formatter addPlaceholder(String placeholder, Object replacement) {
return addContext(new SinglePlaceholder(placeholder, replacement));
}
default Formatter addPlaceholder(String placeholder, Supplier<Object> replacementSupplier, String reLookup) {
return addContext(new SinglePlaceholder(placeholder, replacementSupplier, reLookup));
}
default Formatter addPlaceholder(String placeholder, Supplier<Object> replacementSupplier) {
return addContext(new SinglePlaceholder(placeholder, replacementSupplier));
}

View File

@ -23,9 +23,12 @@
package com.discordsrv.api.events.discord.interaction.command;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordChannel;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
import com.discordsrv.api.discord.entity.guild.DiscordRole;
import com.discordsrv.api.discord.entity.interaction.DiscordInteractionHook;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
@ -39,7 +42,10 @@ import java.util.concurrent.CompletableFuture;
public abstract class AbstractCommandInteractionEvent<E extends GenericCommandInteractionEvent>
extends AbstractDeferrableInteractionEvent<E> {
private final DiscordSRVApi discordSRV;
public AbstractCommandInteractionEvent(
DiscordSRVApi discordSRV,
E jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
@ -48,6 +54,7 @@ public abstract class AbstractCommandInteractionEvent<E extends GenericCommandIn
DiscordInteractionHook interaction
) {
super(jdaEvent, identifier, user, member, channel, interaction);
this.discordSRV = discordSRV;
}
public abstract CompletableFuture<DiscordInteractionHook> reply(SendableDiscordMessage message, boolean ephemeral);
@ -57,8 +64,65 @@ public abstract class AbstractCommandInteractionEvent<E extends GenericCommandIn
}
@Nullable
public String getOption(String name) {
public String getOptionAsString(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
return mapping != null ? mapping.getAsString() : null;
}
@Nullable
public DiscordUser getOptionAsUser(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
long id = mapping.getAsLong();
return discordSRV.discordAPI().getUserById(id);
}
@Nullable
public DiscordRole getOptionAsRole(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
long id = mapping.getAsLong();
return discordSRV.discordAPI().getRoleById(id);
}
@Nullable
public DiscordChannel getOptionAsChannel(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
long id = mapping.getAsLong();
return discordSRV.discordAPI().getChannelById(id);
}
@Nullable
public Long getOptionAsLong(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
return mapping.getAsLong();
}
@Nullable
public Double getOptionAsDouble(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
return mapping.getAsDouble();
}
@Nullable
public Boolean getOptionAsBoolean(String name) {
OptionMapping mapping = jdaEvent.getOption(name);
if (mapping == null) {
return null;
}
return mapping.getAsBoolean();
}
}

View File

@ -23,6 +23,7 @@
package com.discordsrv.api.events.discord.interaction.command;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
@ -33,6 +34,7 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
public abstract class DiscordChatInputInteractionEvent extends AbstractCommandInteractionEvent<SlashCommandInteractionEvent> {
public DiscordChatInputInteractionEvent(
DiscordSRVApi discordSRV,
SlashCommandInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
@ -40,6 +42,6 @@ public abstract class DiscordChatInputInteractionEvent extends AbstractCommandIn
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -23,6 +23,7 @@
package com.discordsrv.api.events.discord.interaction.command;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
@ -34,6 +35,7 @@ public abstract class DiscordMessageContextInteractionEvent
extends AbstractCommandInteractionEvent<MessageContextInteractionEvent> {
public DiscordMessageContextInteractionEvent(
DiscordSRVApi discordSRV,
MessageContextInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
@ -41,6 +43,6 @@ public abstract class DiscordMessageContextInteractionEvent
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -23,6 +23,7 @@
package com.discordsrv.api.events.discord.interaction.command;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.entity.guild.DiscordGuildMember;
@ -33,6 +34,7 @@ import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEven
public abstract class DiscordUserContextInteractionEvent extends AbstractCommandInteractionEvent<UserContextInteractionEvent> {
public DiscordUserContextInteractionEvent(
DiscordSRVApi discordSRV,
UserContextInteractionEvent jdaEvent,
ComponentIdentifier identifier,
DiscordUser user,
@ -40,6 +42,6 @@ public abstract class DiscordUserContextInteractionEvent extends AbstractCommand
DiscordMessageChannel channel,
DiscordInteractionHook interaction
) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
}
}

View File

@ -26,6 +26,7 @@ package com.discordsrv.api.placeholder.provider;
import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Supplier;
@ -33,26 +34,43 @@ public class SinglePlaceholder implements PlaceholderProvider {
private final String matchPlaceholder;
private final Supplier<Object> resultProvider;
private final String reLookup;
public SinglePlaceholder(String placeholder, Object result) {
this(placeholder, () -> result);
this(placeholder, result, null);
}
public SinglePlaceholder(String placeholder, Object result, String reLookup) {
this(placeholder, () -> result, reLookup);
}
public SinglePlaceholder(String placeholder, Supplier<Object> resultProvider) {
this(placeholder, resultProvider, null);
}
public SinglePlaceholder(String placeholder, Supplier<Object> resultProvider, String reLookup) {
this.matchPlaceholder = placeholder;
this.resultProvider = resultProvider;
this.reLookup = reLookup;
}
@Override
public @NotNull PlaceholderLookupResult lookup(@NotNull String placeholder, @NotNull Set<Object> context) {
if (!placeholder.equals(matchPlaceholder)) {
if (!(reLookup != null ? placeholder.startsWith(matchPlaceholder) : placeholder.equals(matchPlaceholder))) {
return PlaceholderLookupResult.UNKNOWN_PLACEHOLDER;
}
try {
return PlaceholderLookupResult.success(
resultProvider.get()
);
Object result = resultProvider.get();
if (reLookup == null) {
return PlaceholderLookupResult.success(result);
}
String newPlaceholder = reLookup + (placeholder.substring(matchPlaceholder.length()));
Set<Object> newContext = new LinkedHashSet<>();
newContext.add(result);
newContext.addAll(context);
return PlaceholderLookupResult.newLookup(newPlaceholder, newContext);
} catch (Throwable t) {
return PlaceholderLookupResult.lookupFailed(t);
}

View File

@ -65,6 +65,7 @@ import com.discordsrv.common.feature.channel.ChannelLockingModule;
import com.discordsrv.common.feature.channel.TimedUpdaterModule;
import com.discordsrv.common.feature.channel.global.GlobalChannelLookupModule;
import com.discordsrv.common.feature.console.ConsoleModule;
import com.discordsrv.common.feature.customcommands.CustomCommandModule;
import com.discordsrv.common.feature.debug.data.VersionInfo;
import com.discordsrv.common.feature.groupsync.GroupSyncModule;
import com.discordsrv.common.feature.linking.LinkProvider;
@ -595,6 +596,7 @@ public abstract class AbstractDiscordSRV<
registerModule(LinkingModule::new);
registerModule(PresenceUpdaterModule::new);
registerModule(MentionGameRenderingModule::new);
registerModule(CustomCommandModule::new);
// Integrations
registerIntegration("com.discordsrv.common.integration.LuckPermsIntegration");

View File

@ -95,7 +95,7 @@ public class ExecuteCommand implements Consumer<DiscordChatInputInteractionEvent
return;
}
String command = event.getOption("command");
String command = event.getOptionAsString("command");
if (command == null) {
return;
}

View File

@ -63,7 +63,7 @@ import java.util.stream.Collectors;
public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurationLoader<CommentedConfigurationNode>>
implements ConfigManager<T>, ConfigLoaderProvider<LT> {
public static final ThreadLocal<Boolean> CLEAN_MAPPER = ThreadLocal.withInitial(() -> false);
public static final ThreadLocal<Boolean> DEFAULT_CONFIG = ThreadLocal.withInitial(() -> false);
private static final ThreadLocal<Boolean> SAVE_OR_LOAD = ThreadLocal.withInitial(() -> false);
public static NamingScheme NAMING_SCHEME = in -> {
@ -353,19 +353,19 @@ public abstract class ConfigurateConfigManager<T, LT extends AbstractConfigurati
/**
* Gets the default config given the default object from {@link #createConfiguration()}
* @param defaultConfig the object
* @param cleanMapper if options that are marked with {@link DefaultOnly} or serializers that make use of {@link #CLEAN_MAPPER} should be excluded from the node
* @param cleanMapper if options that are marked with {@link DefaultOnly} or serializers that make use of {@link #DEFAULT_CONFIG} should be excluded from the node
* @return the node with the values from the object
* @throws SerializationException if serialization fails
*/
private CommentedConfigurationNode getDefault(T defaultConfig, boolean cleanMapper) throws SerializationException {
try {
if (cleanMapper) {
CLEAN_MAPPER.set(true);
DEFAULT_CONFIG.set(true);
}
return getDefault(defaultConfig, cleanMapper ? cleanObjectMapper() : objectMapper());
} finally {
if (cleanMapper) {
CLEAN_MAPPER.set(false);
DEFAULT_CONFIG.set(false);
}
}
}

View File

@ -46,8 +46,9 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
@Override
public DiscordMessageEmbed.Builder deserialize(Type type, ConfigurationNode node) throws SerializationException {
if (ConfigurateConfigManager.CLEAN_MAPPER.get()) {
return null;
Object raw = node.raw();
if (raw instanceof DiscordMessageEmbed.Builder) {
return (DiscordMessageEmbed.Builder) raw;
}
if (!node.node(map("Enabled")).getBoolean(node.node(map("Enable")).getBoolean(true))) {
return null;
@ -90,10 +91,14 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
@Override
public void serialize(Type type, DiscordMessageEmbed.@Nullable Builder obj, ConfigurationNode node)
throws SerializationException {
if (obj == null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
if (obj == null) {
node.set(null);
return;
}
if (ConfigurateConfigManager.DEFAULT_CONFIG.get()) {
node.raw(obj);
return;
}
node.node(map("Color")).set(obj.getColor());
@ -134,6 +139,11 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
@Override
public DiscordMessageEmbed.Field deserialize(Type type, ConfigurationNode node) {
Object raw = node.raw();
if (raw instanceof DiscordMessageEmbed.Field) {
return (DiscordMessageEmbed.Field) raw;
}
// v1 compat
String footerString = node.getString();
if (footerString != null) {
@ -165,6 +175,10 @@ public class DiscordMessageEmbedSerializer implements TypeSerializer<DiscordMess
node.set(null);
return;
}
if (ConfigurateConfigManager.DEFAULT_CONFIG.get()) {
node.raw(obj);
return;
}
node.node(map("Title")).set(obj.getTitle());
node.node(map("Value")).set(obj.getValue());

View File

@ -47,8 +47,13 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
@Override
public SendableDiscordMessage.Builder deserialize(Type type, ConfigurationNode node)
throws SerializationException {
Object raw = node.raw();
if (raw instanceof SendableDiscordMessage.Builder) {
return (SendableDiscordMessage.Builder) raw;
}
String contentOnly = node.getString();
if (contentOnly != null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
if (contentOnly != null) {
return SendableDiscordMessage.builder()
.setContent(contentOnly);
}
@ -82,10 +87,14 @@ public class SendableDiscordMessageSerializer implements TypeSerializer<Sendable
@Override
public void serialize(Type type, SendableDiscordMessage.@Nullable Builder obj, ConfigurationNode node)
throws SerializationException {
if (obj == null || ConfigurateConfigManager.CLEAN_MAPPER.get()) {
if (obj == null) {
node.set(null);
return;
}
if (ConfigurateConfigManager.DEFAULT_CONFIG.get()) {
node.raw(obj);
return;
}
String webhookUsername = obj.getWebhookUsername();
if (webhookUsername != null) {

View File

@ -0,0 +1,68 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV 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 com.discordsrv.common.config.main;
import com.discordsrv.api.discord.entity.interaction.command.CommandOption;
import com.discordsrv.api.discord.entity.message.DiscordMessageEmbed;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.ArrayList;
import java.util.List;
@ConfigSerializable
public class CustomCommandConfig {
public static CustomCommandConfig defaultIp() {
CustomCommandConfig config = new CustomCommandConfig();
config.command = "ip";
config.description = "Get the Minecraft server ip";
config.ephemeral = true;
config.response = SendableDiscordMessage.builder().setContent("`yourserveripchange.me`");
return config;
}
public static CustomCommandConfig defaultHelloWorld() {
CustomCommandConfig config = new CustomCommandConfig();
config.command = "hello";
config.description = "Greet a user";
config.options.add(new OptionConfig());
config.response = SendableDiscordMessage.builder()
.addEmbed(DiscordMessageEmbed.builder().setDescription("Hello %option_target_user_name%").build());
return config;
}
public String command = "";
public String description = "";
public boolean ephemeral = false;
public List<OptionConfig> options = new ArrayList<>();
public SendableDiscordMessage.Builder response = SendableDiscordMessage.builder().setContent("test");
@ConfigSerializable
public static class OptionConfig {
public CommandOption.Type type = CommandOption.Type.USER;
public String name = "target_user";
public String description = "The user to greet";
public boolean required = true;
}
}

View File

@ -104,6 +104,11 @@ public abstract class MainConfig implements Config {
@Comment("Options for console channel(s) and/or thread(s)")
public List<ConsoleConfig> console = new ArrayList<>(Collections.singleton(new ConsoleConfig()));
public List<CustomCommandConfig> customCommands = new ArrayList<>(Arrays.asList(
CustomCommandConfig.defaultIp(),
CustomCommandConfig.defaultHelloWorld()
));
@Comment("Configuration for the %1 placeholder. The below options will be attempted in the order they are in")
@Constants.Comment("%discord_invite%")
public DiscordInviteConfig invite = new DiscordInviteConfig();

View File

@ -19,7 +19,6 @@
package com.discordsrv.common.config.main.channels;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.config.configurate.annotation.DefaultOnly;
import com.discordsrv.common.config.configurate.annotation.Untranslated;
import com.discordsrv.common.config.configurate.manager.abstraction.ConfigurateConfigManager;
import com.discordsrv.common.config.main.generic.IMessageConfig;
@ -40,7 +39,6 @@ public class MinecraftToDiscordChatConfig implements IMessageConfig {
public Boolean enabled = true;
@Untranslated(Untranslated.Type.VALUE)
@DefaultOnly
public SendableDiscordMessage.Builder format = SendableDiscordMessage.builder()
.setWebhookUsername("%player_meta_prefix|player_prefix%%player_display_name|player_name%%player_meta_suffix|player_suffix%")
.setWebhookAvatarUrl("%player_avatar_url%")

View File

@ -113,6 +113,16 @@ public class DiscordAPIImpl implements DiscordAPI {
return CompletableFutureUtil.failed(new NotReadyException());
}
@Override
public DiscordChannel getChannelById(long id) {
DiscordForumChannel forumChannel = getForumChannelById(id);
if (forumChannel != null) {
return forumChannel;
}
return getMessageChannelById(id);
}
@Override
public @Nullable DiscordMessageChannel getMessageChannelById(long id) {
DiscordTextChannel textChannel = getTextChannelById(id);
@ -130,6 +140,11 @@ public class DiscordAPIImpl implements DiscordAPI {
return voiceChannel;
}
DiscordStageChannel stageChannel = getStageChannelById(id);
if (stageChannel != null) {
return stageChannel;
}
DiscordNewsChannel newsChannel = getNewsChannelById(id);
if (newsChannel != null) {
return newsChannel;
@ -153,12 +168,14 @@ public class DiscordAPIImpl implements DiscordAPI {
return getTextChannel((TextChannel) jda);
} else if (jda instanceof ThreadChannel) {
return getThreadChannel((ThreadChannel) jda);
} else if (jda instanceof PrivateChannel) {
return getDirectMessageChannel((PrivateChannel) jda);
} else if (jda instanceof NewsChannel) {
return getNewsChannel((NewsChannel) jda);
} else if (jda instanceof VoiceChannel) {
return getVoiceChannel((VoiceChannel) jda);
} else if (jda instanceof StageChannel) {
return getStageChannel((StageChannel) jda);
} else if (jda instanceof NewsChannel) {
return getNewsChannel((NewsChannel) jda);
} else if (jda instanceof PrivateChannel) {
return getDirectMessageChannel((PrivateChannel) jda);
} else {
throw new IllegalArgumentException("Unmappable MessageChannel type: " + jda.getClass().getName());
}
@ -189,7 +206,7 @@ public class DiscordAPIImpl implements DiscordAPI {
@Override
public @Nullable DiscordNewsChannel getNewsChannelById(long id) {
return null;
return mapJDAEntity(jda -> jda.getNewsChannelById(id), this::getNewsChannel);
}
public DiscordNewsChannelImpl getNewsChannel(NewsChannel jda) {

View File

@ -43,7 +43,7 @@ public class DiscordChatInputInteractionEventImpl extends DiscordChatInputIntera
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel, DiscordInteractionHook interaction) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
this.discordSRV = discordSRV;
}

View File

@ -43,7 +43,7 @@ public class DiscordMessageContextInteractionEventImpl extends DiscordMessageCon
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel, DiscordInteractionHook interaction) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
this.discordSRV = discordSRV;
}

View File

@ -43,7 +43,7 @@ public class DiscordUserContextInteractionEventImpl extends DiscordUserContextIn
DiscordUser user,
DiscordGuildMember member,
DiscordMessageChannel channel, DiscordInteractionHook interaction) {
super(jdaEvent, identifier, user, member, channel, interaction);
super(discordSRV, jdaEvent, identifier, user, member, channel, interaction);
this.discordSRV = discordSRV;
}

View File

@ -0,0 +1,233 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV 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 com.discordsrv.common.feature.customcommands;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.entity.DiscordUser;
import com.discordsrv.api.discord.entity.channel.DiscordChannel;
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.DiscordCommand;
import com.discordsrv.api.discord.entity.interaction.component.ComponentIdentifier;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.CustomCommandConfig;
import com.discordsrv.common.core.logging.NamedLogger;
import com.discordsrv.common.core.module.type.AbstractModule;
import org.apache.commons.lang3.StringUtils;
import org.intellij.lang.annotations.Subst;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public class CustomCommandModule extends AbstractModule<DiscordSRV> {
private final List<DiscordCommand> registeredCommands = new ArrayList<>();
public CustomCommandModule(DiscordSRV discordSRV) {
super(discordSRV, new NamedLogger(discordSRV, "CUSTOM_COMMANDS"));
}
@Override
public boolean canEnableBeforeReady() {
return discordSRV.config() != null;
}
@Override
public void reload(Consumer<DiscordSRVApi.ReloadResult> resultConsumer) {
List<CustomCommandConfig> configs = discordSRV.config().customCommands;
List<LayerCommand> layeredCommands = new ArrayList<>();
int i = 0;
for (CustomCommandConfig config : configs) {
List<String> commandParts = Arrays.asList(config.command.split(" "));
int parts = commandParts.size();
if (parts > 3) {
logger().error("Invalid command (" + config.command + "), too many parts: " + parts);
continue;
}
if (StringUtils.isEmpty(config.description)) {
logger().error("Invalid command (" + config.command + "): empty description");
continue;
}
String prefixOrMainCommand = String.join(" ", commandParts.subList(0, Math.max(parts - 1, 1)));
if (parts > 2) {
String group = commandParts.get(0);
if (layeredCommands.stream().anyMatch(command -> command.getPrefix().equals(group))) {
logger().error("Cannot use sub command group, sub command already being used: " + group); // TODO: better error
continue;
}
}
@Subst("ip")
String name = commandParts.get(parts - 1);
DiscordCommand.ChatInputBuilder commandBuilder = DiscordCommand.chatInput(
ComponentIdentifier.of("DiscordSRV", "custom-command-" + (++i)),
name,
config.description
);
for (CustomCommandConfig.OptionConfig option : config.options) {
if (StringUtils.isEmpty(option.description)) {
logger().error("Invalid command option (" + option.name + " of " + config.command + "): empty description");
continue;
}
commandBuilder.addOption(
CommandOption.builder(option.type, option.name, option.description)
.setRequired(option.required)
.build()
);
}
commandBuilder.setEventHandler(event -> {
SendableDiscordMessage.Formatter formatter = config.response.toFormatter();
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();
LayerCommand foundLayer = layeredCommands.stream()
.filter(cmd -> cmd.getLayer() == parts)
.filter(cmd -> cmd.getPrefix().equals(prefixOrMainCommand))
.findAny().orElse(null);
if (foundLayer != null) {
if (parts == 1) {
logger().error("Duplicate main command: " + commandParts.get(0));
continue;
}
foundLayer.getCommands().add(command);
continue;
}
layeredCommands.add(new LayerCommand(
prefixOrMainCommand,
parts,
new ArrayList<>(Collections.singleton(command))
));
}
List<DiscordCommand> commandsToRegister = new ArrayList<>();
for (LayerCommand layeredCommand : layeredCommands) {
commandsToRegister.addAll(layeredCommand.getCommands());
}
for (DiscordCommand command : registeredCommands) {
discordSRV.discordAPI().unregisterCommand(command);
}
registeredCommands.clear();
registeredCommands.addAll(commandsToRegister);
for (DiscordCommand command : commandsToRegister) {
DiscordCommand.RegistrationResult registrationResult = discordSRV.discordAPI().registerCommand(command);
logger().debug("Registration of " + command.getName() + ": " + registrationResult.name());
}
}
public static class LayerCommand {
private final String prefix;
private final int layer;
private final List<DiscordCommand> commands;
public LayerCommand(String prefix, int layer, List<DiscordCommand> commands) {
this.prefix = prefix;
this.layer = layer;
this.commands = commands;
}
public String getPrefix() {
return prefix;
}
public int getLayer() {
return layer;
}
public List<DiscordCommand> getCommands() {
return commands;
}
}
}