From ee6456e4d8d476a51dabf2b487aeecbff6eca573 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 30 Jun 2024 05:15:37 +0100 Subject: [PATCH] add feature toggling --- .../cc/fascinated/bat/BatApplication.java | 2 +- .../cc/fascinated/bat/command/BatCommand.java | 6 ++ .../cc/fascinated/bat/features/Feature.java | 1 + .../bat/features/FeatureProfile.java | 57 ++++++++++++++++++ .../bat/features/afk/profile/AfkProfile.java | 4 ++ .../features/autorole/AutoRoleFeature.java | 2 +- .../bat/features/birthday/UserBirthday.java | 3 - .../features/command/DisableSubCommand.java | 58 +++++++++++++++++++ .../features/command/EnableSubCommand.java | 58 +++++++++++++++++++ .../bat/features/command/FeatureCommand.java | 23 ++++++++ .../bat/features/command/ListSubCommand.java | 45 ++++++++++++++ .../cc/fascinated/bat/model/BatGuild.java | 10 ++++ .../bat/service/CommandService.java | 17 ++++-- .../bat/service/FeatureService.java | 40 ++++++++++++- 14 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 src/main/java/cc/fascinated/bat/features/FeatureProfile.java create mode 100644 src/main/java/cc/fascinated/bat/features/command/DisableSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/command/EnableSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/command/FeatureCommand.java create mode 100644 src/main/java/cc/fascinated/bat/features/command/ListSubCommand.java diff --git a/src/main/java/cc/fascinated/bat/BatApplication.java b/src/main/java/cc/fascinated/bat/BatApplication.java index 6a3716d..0d03970 100644 --- a/src/main/java/cc/fascinated/bat/BatApplication.java +++ b/src/main/java/cc/fascinated/bat/BatApplication.java @@ -17,7 +17,7 @@ import java.nio.file.StandardCopyOption; import java.util.Objects; @EnableScheduling -@SpringBootApplication +@SpringBootApplication(scanBasePackages = "cc.fascinated.bat") @EnableMongock @Log4j2(topic = "Bat") public class BatApplication { diff --git a/src/main/java/cc/fascinated/bat/command/BatCommand.java b/src/main/java/cc/fascinated/bat/command/BatCommand.java index a1af652..e5842c5 100644 --- a/src/main/java/cc/fascinated/bat/command/BatCommand.java +++ b/src/main/java/cc/fascinated/bat/command/BatCommand.java @@ -1,5 +1,6 @@ package cc.fascinated.bat.command; +import cc.fascinated.bat.features.Feature; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -38,6 +39,11 @@ public abstract class BatCommand implements BatCommandExecutor { */ private Category category; + /** + * The feature that the command belongs to + */ + private Feature feature; + /** * Whether the command can only be used by the bot owner */ diff --git a/src/main/java/cc/fascinated/bat/features/Feature.java b/src/main/java/cc/fascinated/bat/features/Feature.java index ecdcfd7..da16f58 100644 --- a/src/main/java/cc/fascinated/bat/features/Feature.java +++ b/src/main/java/cc/fascinated/bat/features/Feature.java @@ -33,6 +33,7 @@ public abstract class Feature { */ public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) { command.setCategory(category); + command.setFeature(this); commandService.registerCommand(command); } } diff --git a/src/main/java/cc/fascinated/bat/features/FeatureProfile.java b/src/main/java/cc/fascinated/bat/features/FeatureProfile.java new file mode 100644 index 0000000..c829ab2 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/FeatureProfile.java @@ -0,0 +1,57 @@ +package cc.fascinated.bat.features; + +import cc.fascinated.bat.common.Profile; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Fascinated (fascinated7) + */ +public class FeatureProfile extends Profile { + /** + * The feature states + */ + private Map featureStates; + + public FeatureProfile() { + super("feature"); + } + + /** + * Gets the feature states + * + * @return the feature states + */ + public boolean isFeatureDisabled(Feature feature) { + if (this.featureStates == null) { + this.featureStates = new HashMap<>(); + } + if (feature == null) { + return false; + } + String featureName = feature.getName().toUpperCase(); + if (!this.featureStates.containsKey(featureName)) { + this.featureStates.put(featureName, false); + } + return this.featureStates.get(featureName); + } + + /** + * Sets the feature state + * + * @param feature the feature to set the state for + * @param state the state to set + */ + public void setFeatureState(Feature feature, boolean state) { + if (this.featureStates == null) { + this.featureStates = new HashMap<>(); + } + this.featureStates.put(feature.getName().toUpperCase(), state); + } + + @Override + public void reset() { + this.featureStates = null; + } +} diff --git a/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java b/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java index 20aeecb..06f60ba 100644 --- a/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java +++ b/src/main/java/cc/fascinated/bat/features/afk/profile/AfkProfile.java @@ -21,6 +21,10 @@ public class AfkProfile extends Profile { */ private Map afkUsers; + public AfkProfile() { + super("afk"); + } + /** * Adds a user to the AFK list * diff --git a/src/main/java/cc/fascinated/bat/features/autorole/AutoRoleFeature.java b/src/main/java/cc/fascinated/bat/features/autorole/AutoRoleFeature.java index a33b3e0..bed6c49 100644 --- a/src/main/java/cc/fascinated/bat/features/autorole/AutoRoleFeature.java +++ b/src/main/java/cc/fascinated/bat/features/autorole/AutoRoleFeature.java @@ -16,7 +16,7 @@ import org.springframework.stereotype.Component; public class AutoRoleFeature extends Feature { @Autowired public AutoRoleFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { - super("AutoRole", Category.SERVER); + super("Auto Role", Category.SERVER); registerCommand(commandService, context.getBean(AutoRoleCommand.class)); } diff --git a/src/main/java/cc/fascinated/bat/features/birthday/UserBirthday.java b/src/main/java/cc/fascinated/bat/features/birthday/UserBirthday.java index 63ca047..ab0d138 100644 --- a/src/main/java/cc/fascinated/bat/features/birthday/UserBirthday.java +++ b/src/main/java/cc/fascinated/bat/features/birthday/UserBirthday.java @@ -34,16 +34,13 @@ public class UserBirthday { public int calculateAge() { Calendar birthdayCalendar = Calendar.getInstance(); birthdayCalendar.setTime(this.getBirthday()); - Calendar today = Calendar.getInstance(); - int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR); // Check if the birthday hasn't occurred yet this year if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) { age--; } - return age; } } diff --git a/src/main/java/cc/fascinated/bat/features/command/DisableSubCommand.java b/src/main/java/cc/fascinated/bat/features/command/DisableSubCommand.java new file mode 100644 index 0000000..ca9fc0d --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/command/DisableSubCommand.java @@ -0,0 +1,58 @@ +package cc.fascinated.bat.features.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.FeatureProfile; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.FeatureService; +import cc.fascinated.bat.service.GuildService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component("feature:disable.sub") +@CommandInfo(name = "disable", description = "Disables a feature") +public class DisableSubCommand extends BatSubCommand { + private final GuildService guildService; + + @Autowired + public DisableSubCommand(@NonNull GuildService guildService) { + this.guildService = guildService; + super.addOption(OptionType.STRING, "feature", "The feature to disable", true); + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { + FeatureProfile featureProfile = guild.getFeatureProfile(); + OptionMapping featureOption = interaction.getOption("feature"); + if (featureOption == null) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("You must provide a feature to disable").build()).queue(); + return; + } + String featureName = featureOption.getAsString(); + if (!FeatureService.INSTANCE.isFeature(featureName)) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("That feature does not exist").build()).queue(); + return; + } + Feature feature = FeatureService.INSTANCE.getFeature(featureName); + if (featureProfile.isFeatureDisabled(feature)) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("That feature is already disabled").build()).queue(); + return; + } + + featureProfile.setFeatureState(feature, true); + guildService.saveGuild(guild); + interaction.replyEmbeds(EmbedUtils.successEmbed().setDescription("Successfully disabled the feature " + feature.getName()).build()).queue(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/command/EnableSubCommand.java b/src/main/java/cc/fascinated/bat/features/command/EnableSubCommand.java new file mode 100644 index 0000000..7346466 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/command/EnableSubCommand.java @@ -0,0 +1,58 @@ +package cc.fascinated.bat.features.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.FeatureProfile; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.FeatureService; +import cc.fascinated.bat.service.GuildService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component("feature:enable.sub") +@CommandInfo(name = "enable", description = "Enables a feature") +public class EnableSubCommand extends BatSubCommand { + private final GuildService guildService; + + @Autowired + public EnableSubCommand(@NonNull GuildService guildService) { + this.guildService = guildService; + super.addOption(OptionType.STRING, "feature", "The feature to enable", true); + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { + FeatureProfile featureProfile = guild.getFeatureProfile(); + OptionMapping featureOption = interaction.getOption("feature"); + if (featureOption == null) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("You must provide a feature to enable").build()).queue(); + return; + } + String featureName = featureOption.getAsString(); + if (!FeatureService.INSTANCE.isFeature(featureName)) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("That feature does not exist").build()).queue(); + return; + } + Feature feature = FeatureService.INSTANCE.getFeature(featureName); + if (!featureProfile.isFeatureDisabled(feature)) { + interaction.replyEmbeds(EmbedUtils.errorEmbed().setDescription("That feature is already enabled").build()).queue(); + return; + } + + featureProfile.setFeatureState(feature, false); + guildService.saveGuild(guild); + interaction.replyEmbeds(EmbedUtils.successEmbed().setDescription("Successfully enabled the feature " + feature.getName()).build()).queue(); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/command/FeatureCommand.java b/src/main/java/cc/fascinated/bat/features/command/FeatureCommand.java new file mode 100644 index 0000000..36c41d7 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/command/FeatureCommand.java @@ -0,0 +1,23 @@ +package cc.fascinated.bat.features.command; + +import cc.fascinated.bat.command.BatCommand; +import cc.fascinated.bat.command.CommandInfo; +import lombok.NonNull; +import net.dv8tion.jda.api.Permission; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component +@CommandInfo(name = "features", description = "Configure features in your guild", requiredPermissions = Permission.ADMINISTRATOR) +public class FeatureCommand extends BatCommand { + @Autowired + public FeatureCommand(@NonNull ApplicationContext context) { + super.addSubCommand(context.getBean(EnableSubCommand.class)); + super.addSubCommand(context.getBean(DisableSubCommand.class)); + super.addSubCommand(context.getBean(ListSubCommand.class)); + } +} diff --git a/src/main/java/cc/fascinated/bat/features/command/ListSubCommand.java b/src/main/java/cc/fascinated/bat/features/command/ListSubCommand.java new file mode 100644 index 0000000..0a29881 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/command/ListSubCommand.java @@ -0,0 +1,45 @@ +package cc.fascinated.bat.features.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.FeatureProfile; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.FeatureService; +import cc.fascinated.bat.service.GuildService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Fascinated (fascinated7) + */ +@Component("feature:list.sub") +@CommandInfo(name = "list", description = "Lists the features and their states") +public class ListSubCommand extends BatSubCommand { + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { + StringBuilder featureStates = new StringBuilder(); + for (Feature feature : FeatureService.INSTANCE.getFeatures().values()) { + FeatureProfile featureProfile = guild.getFeatureProfile(); + if (featureProfile.isFeatureDisabled(feature)) { + featureStates.append("❌ ").append(feature.getName()).append("\n"); + } else { + featureStates.append("✅ ").append(feature.getName()).append("\n"); + } + } + + interaction.replyEmbeds(EmbedUtils.genericEmbed() + .setTitle("Feature List") + .setDescription(featureStates.toString()) + .build() + ).queue(); + } +} diff --git a/src/main/java/cc/fascinated/bat/model/BatGuild.java b/src/main/java/cc/fascinated/bat/model/BatGuild.java index ebf1558..d3d4322 100644 --- a/src/main/java/cc/fascinated/bat/model/BatGuild.java +++ b/src/main/java/cc/fascinated/bat/model/BatGuild.java @@ -1,6 +1,7 @@ package cc.fascinated.bat.model; import cc.fascinated.bat.common.ProfileHolder; +import cc.fascinated.bat.features.FeatureProfile; import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile; import cc.fascinated.bat.service.DiscordService; import lombok.*; @@ -75,6 +76,15 @@ public class BatGuild extends ProfileHolder { return getProfile(NameHistoryProfile.class); } + /** + * Gets the feature profile + * + * @return the feature profile + */ + public FeatureProfile getFeatureProfile() { + return getProfile(FeatureProfile.class); + } + @AllArgsConstructor @Getter @Setter diff --git a/src/main/java/cc/fascinated/bat/service/CommandService.java b/src/main/java/cc/fascinated/bat/service/CommandService.java index d00e92d..e7da293 100644 --- a/src/main/java/cc/fascinated/bat/service/CommandService.java +++ b/src/main/java/cc/fascinated/bat/service/CommandService.java @@ -4,6 +4,7 @@ import cc.fascinated.bat.Consts; import cc.fascinated.bat.command.*; import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.config.Config; +import cc.fascinated.bat.features.FeatureProfile; import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatUser; import lombok.Getter; @@ -81,9 +82,8 @@ public class CommandService extends ListenerAdapter { return; } jda.retrieveCommands().complete().forEach(command -> jda.deleteCommandById(command.getId()).complete()); - adminGuild.updateCommands().addCommands(commands.values().stream() - .map(BatCommand::getCommandData).toList()).complete(); - log.info("Registered {} slash commands in {}ms (DEV MODE)", commands.size(), System.currentTimeMillis() - before); + List registeredCommands = adminGuild.updateCommands().addCommands(commands.values().stream().map(BatCommand::getCommandData).toList()).complete(); + log.info("Registered {} slash commands in {}ms (DEV MODE)", registeredCommands.size(), System.currentTimeMillis() - before); return; } @@ -123,7 +123,6 @@ public class CommandService extends ListenerAdapter { } else { log.error("Unable to find the admin guild to register hidden commands"); } - log.info("Registered {} slash commands in {}ms", discordCommands.size(), System.currentTimeMillis() - before); } @@ -221,6 +220,16 @@ public class CommandService extends ListenerAdapter { return; } + if (guild != null) { + FeatureProfile featureProfile = guild.getFeatureProfile(); + if (featureProfile.isFeatureDisabled(command.getFeature())) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("This command has been disabled by the guild owner") + .build()).setEphemeral(true).queue(); + return; + } + } + log.info("Executing command \"{}\" for user \"{}\"", commandName, user.getDiscordUser().getName()); executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(), event.getMember(), event.getInteraction()); diff --git a/src/main/java/cc/fascinated/bat/service/FeatureService.java b/src/main/java/cc/fascinated/bat/service/FeatureService.java index 4eb7627..650c8ef 100644 --- a/src/main/java/cc/fascinated/bat/service/FeatureService.java +++ b/src/main/java/cc/fascinated/bat/service/FeatureService.java @@ -2,6 +2,8 @@ package cc.fascinated.bat.service; import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.command.FeatureCommand; +import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.NonNull; import lombok.extern.log4j.Log4j2; @@ -11,7 +13,9 @@ import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author Fascinated (fascinated7) @@ -21,17 +25,28 @@ import java.util.List; @Log4j2 @DependsOn("commandService") public class FeatureService { + public static FeatureService INSTANCE; + private final ApplicationContext context; + private final CommandService commandService; + /** * The registered features */ - private final List features = new ArrayList<>(); + private final Map features = new HashMap<>(); @Autowired public FeatureService(@NonNull ApplicationContext context, @NonNull CommandService commandService) { + this.context = context; + this.commandService = commandService; + INSTANCE = this; + } + + @PostConstruct + public void init() { context.getBeansOfType(Feature.class) .values() .forEach((feature) -> { - features.add(context.getBean(feature.getClass())); + features.put(feature.getName().toLowerCase(), feature); }); context.getBeansOfType(BatCommand.class) @@ -40,6 +55,27 @@ public class FeatureService { commandService.registerCommand(context.getBean(command.getClass())); }); + commandService.registerCommand(context.getBean(FeatureCommand.class)); commandService.registerSlashCommands(); // Register all slash commands } + + /** + * Gets a feature by name + * + * @param name The name of the feature + * @return The feature + */ + public Feature getFeature(@NonNull String name) { + return features.get(name.toLowerCase()); + } + + /** + * Checks if a feature is registered + * + * @param name The name of the feature + * @return Whether the feature is registered + */ + public boolean isFeature(@NonNull String name) { + return features.containsKey(name.toLowerCase()); + } }