Skip to content

Commit

Permalink
Created Slash command framework
Browse files Browse the repository at this point in the history
Added slotting slash command
  • Loading branch information
Alf-Melmac committed Jul 27, 2021
1 parent 0740433 commit 62a576a
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 10 deletions.
9 changes: 5 additions & 4 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/main/java/de/webalf/slotbot/constant/Emojis.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
*/
@Value
public class Emojis {
//Codepoint notation
public static final String THUMBS_UP = "U+1F44D";
public static final String THUMBS_DOWN = "U+1F44E";

//Standard discord notation
public static final String CHECKBOX = ":ballot_box_with_check:";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.webalf.slotbot.model.annotations;

import de.webalf.slotbot.util.permissions.BotPermissionHelper.Authorization;
import org.atteo.classindex.IndexAnnotated;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static de.webalf.slotbot.util.permissions.BotPermissionHelper.Authorization.ADMINISTRATIVE;

/**
* @author Alf
* @since 15.07.2021
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@IndexAnnotated
public @interface SlashCommand {
String name();

String description();

Authorization authorization() default ADMINISTRATIVE;

int optionPosition() default -1;
}
7 changes: 6 additions & 1 deletion src/main/java/de/webalf/slotbot/service/bot/BotService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package de.webalf.slotbot.service.bot;

import de.webalf.slotbot.configuration.properties.DiscordProperties;
import de.webalf.slotbot.service.bot.listener.GuildReadyListener;
import de.webalf.slotbot.service.bot.listener.MessageReceivedListener;
import de.webalf.slotbot.service.bot.listener.ReactionAddListener;
import de.webalf.slotbot.service.bot.listener.SlashCommandListener;
import de.webalf.slotbot.util.bot.CommandClassHelper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -27,6 +29,7 @@ public class BotService {
private final DiscordProperties discordProperties;
private final CommandClassHelper commandClassHelper;
private final ReactionAddService reactionAddService;
private final SlashCommandsService slashCommandsService;

@Getter
private JDA jda;
Expand All @@ -46,7 +49,9 @@ public void startUp() {
.enableIntents(GUILD_MEMBERS)
.addEventListeners(
new MessageReceivedListener(discordProperties, commandClassHelper),
new ReactionAddListener(reactionAddService))
new ReactionAddListener(reactionAddService),
new GuildReadyListener(slashCommandsService),
new SlashCommandListener(commandClassHelper))
.disableIntents(GUILD_BANS, GUILD_EMOJIS, GUILD_INVITES, GUILD_VOICE_STATES, GUILD_MESSAGE_REACTIONS, GUILD_MESSAGE_TYPING, DIRECT_MESSAGE_TYPING)
.build();
} catch (LoginException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package de.webalf.slotbot.service.bot;

import de.webalf.slotbot.model.annotations.SlashCommand;
import de.webalf.slotbot.util.bot.CommandClassHelper;
import de.webalf.slotbot.util.bot.SlashCommandUtils;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static de.webalf.slotbot.util.bot.CommandClassHelper.getSlashCommand;
import static de.webalf.slotbot.util.bot.SlashCommandUtils.getCommandPrivileges;
import static de.webalf.slotbot.util.permissions.BotPermissionHelper.Authorization.NONE;

/**
* @author Alf
* @since 15.07.2021
*/
@Service
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SlashCommandsService {
private final CommandClassHelper commandClassHelper;

/**
* Updates slash commands in the given guild
*
* @param guild to update commands for
*/
public void updateCommands(@NonNull Guild guild) {
final List<CommandData> commands = SlashCommandUtils.commandToClassMap.values().stream()
.map(slashCommandClass -> { //For each slash command
final SlashCommand slashCommand = getSlashCommand(slashCommandClass);
final CommandData commandData = new CommandData(slashCommand.name().toLowerCase(), slashCommand.description()); //Create Command data
if (slashCommand.optionPosition() >= 0) { //Add options if present
commandData.addOptions(getOptions(slashCommandClass, slashCommand.optionPosition()));
}
if (slashCommand.authorization() != NONE) {
commandData.setDefaultEnabled(false);
}
return commandData;
}).collect(Collectors.toUnmodifiableList());

guild.updateCommands().addCommands(commands).queue(updatedCommands -> updatedCommands.forEach(command -> { //Update discord commands
final SlashCommand slashCommand = getSlashCommand(SlashCommandUtils.get(command.getName()));
if (slashCommand.authorization() != NONE) { //Set authorized roles if needed
guild.updateCommandPrivilegesById(command.getIdLong(), getCommandPrivileges(guild, slashCommand)).queue();
}
}));
}

private List<OptionData> getOptions(Class<?> commandClass, int optionPosition) {
try {
//noinspection unchecked The class must implement an interface and thus we can assume the correct return type here
return (List<OptionData>) commandClass.getMethod("getOptions", int.class).invoke(commandClassHelper.getConstructor(commandClass), optionPosition);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
log.error("Failed to getOptions {}", e.getMessage());
return Collections.emptyList();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.webalf.slotbot.service.bot.command;

import de.webalf.slotbot.model.annotations.SlashCommand;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;

import java.util.List;

/**
* @author Alf
* @since 18.07.2021
*/
public interface DiscordSlashCommand {
void execute(SlashCommandEvent event);

/**
* List of all slash command options. For each slash command the index in this list is specified in {@link SlashCommand#optionPosition()}
*
* @return list of every option of all slash commands
*/
@SuppressWarnings("unused") //Used by SlashCommandsService#getOptions(Class, int)
List<OptionData> getOptions(int optionPosition);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package de.webalf.slotbot.service.bot.command.event;

import de.webalf.slotbot.model.annotations.Command;
import de.webalf.slotbot.model.annotations.SlashCommand;
import de.webalf.slotbot.service.bot.EventBotService;
import de.webalf.slotbot.service.bot.command.DiscordCommand;
import de.webalf.slotbot.service.bot.command.DiscordSlashCommand;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;

import javax.validation.constraints.NotBlank;
import java.util.List;

import static de.webalf.slotbot.util.ListUtils.oneArgument;
import static de.webalf.slotbot.util.StringUtils.onlyNumbers;
import static de.webalf.slotbot.util.bot.InteractionUtils.finishedSlashCommandAction;
import static de.webalf.slotbot.util.bot.MentionUtils.getId;
import static de.webalf.slotbot.util.bot.MentionUtils.isUserMention;
import static de.webalf.slotbot.util.bot.MessageUtils.deleteMessagesInstant;
Expand All @@ -33,7 +39,11 @@
usage = "<Slotnummer> (<@ZuSlottendePerson>)",
argCount = {1, 2},
authorization = NONE)
public class Slot implements DiscordCommand {
@SlashCommand(name = "slot",
description = "Slottet dich in ein Event.",
authorization = NONE,
optionPosition = 0)
public class Slot implements DiscordCommand, DiscordSlashCommand {
private final EventBotService eventBotService;

@Override
Expand Down Expand Up @@ -79,4 +89,29 @@ private void slot(@NonNull Message message, @NotBlank String slot, String userId
private void selfSlot(@NonNull Message message, @NotBlank String slot) {
slot(message, slot, message.getAuthor().getId());
}

static final String OPTION_SLOT_NUMBER = "slotnummer";
static final List<List<OptionData>> SLOT_OPTIONS = List.of(
List.of(new OptionData(OptionType.INTEGER, OPTION_SLOT_NUMBER, "Nummer des erwünschten Slots.", true))
);

@Override
public void execute(SlashCommandEvent event) {
log.trace("Slash command: slot");

@SuppressWarnings("ConstantConditions") //Required option
final int slotNumber = Math.toIntExact(event.getOption(OPTION_SLOT_NUMBER).getAsLong());
selfSlot(event, slotNumber);

finishedSlashCommandAction(event);
}

@Override
public List<OptionData> getOptions(int optionPosition) {
return SLOT_OPTIONS.get(optionPosition);
}

private void selfSlot(SlashCommandEvent event, int slotNumber) {
eventBotService.slot(event.getChannel().getIdLong(), slotNumber, event.getUser().getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.webalf.slotbot.service.bot.listener;

import de.webalf.slotbot.service.bot.SlashCommandsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;

/**
* @author Alf
* @since 15.07.2021
*/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GuildReadyListener extends ListenerAdapter {
private final SlashCommandsService slashCommandsService;

@Override
public void onGuildReady(@NotNull GuildReadyEvent event) {
slashCommandsService.updateCommands(event.getGuild());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package de.webalf.slotbot.service.bot.listener;

import de.webalf.slotbot.exception.BusinessRuntimeException;
import de.webalf.slotbot.exception.ForbiddenException;
import de.webalf.slotbot.exception.ResourceNotFoundException;
import de.webalf.slotbot.util.StringUtils;
import de.webalf.slotbot.util.bot.CommandClassHelper;
import de.webalf.slotbot.util.bot.SlashCommandUtils;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.InvocationTargetException;

import static de.webalf.slotbot.util.bot.InteractionUtils.ephemeralDeferReply;
import static de.webalf.slotbot.util.bot.InteractionUtils.reply;

/**
* @author Alf
* @since 15.07.2021
*/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SlashCommandListener extends ListenerAdapter {
private final CommandClassHelper commandClassHelper;

@Override
public void onSlashCommand(@NonNull SlashCommandEvent event) {
final String commandName = event.getName();
log.debug("Received slash command: {} from {}", commandName, event.getUser().getId());

final Class<?> commandClass = SlashCommandUtils.get(commandName);
if (commandClass == null) {
log.error("Received not known slash command: {}", commandName);
return;
}

ephemeralDeferReply(event);

try {
commandClass.getMethod("execute", SlashCommandEvent.class).invoke(commandClassHelper.getConstructor(commandClass), event);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof BusinessRuntimeException || cause instanceof ForbiddenException || cause instanceof ResourceNotFoundException) {
if (StringUtils.isNotEmpty(cause.getMessage())) {
reply(event, cause.getMessage());
} else {
reply(event, "Das gesuchte Element kann nicht erreicht werden.");
}
} else {
unknownException(event, commandClass, e);
}
} catch (NoSuchMethodException | IllegalAccessException e) {
unknownException(event, commandClass, e);
}
}

private void unknownException(SlashCommandEvent event, @NonNull Class<?> commandClass, ReflectiveOperationException e) {
log.error("Failed to execute slash command {} with options {}", commandClass.getName(), event.getOptions(), e);
reply(event, "Tja, da ist wohl was schief gelaufen.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.webalf.slotbot.configuration.properties.DiscordProperties;
import de.webalf.slotbot.model.annotations.Command;
import de.webalf.slotbot.model.annotations.SlashCommand;
import de.webalf.slotbot.service.bot.EventBotService;
import de.webalf.slotbot.service.bot.SlotBotService;
import de.webalf.slotbot.service.bot.UserBotService;
Expand Down Expand Up @@ -97,4 +98,8 @@ public Object getConstructor(@NonNull Class<?> commandClass) throws IllegalArgum
public static Command getCommand(@NonNull Class<?> commandClass) {
return commandClass.getAnnotation(Command.class);
}

public static SlashCommand getSlashCommand(@NonNull Class<?> commandClass) {
return commandClass.getAnnotation(SlashCommand.class);
}
}

0 comments on commit 62a576a

Please sign in to comment.