add feature toggling

This commit is contained in:
Lee 2024-06-30 05:15:37 +01:00
parent 93350f1506
commit ee6456e4d8
14 changed files with 315 additions and 11 deletions

@ -17,7 +17,7 @@ import java.nio.file.StandardCopyOption;
import java.util.Objects; import java.util.Objects;
@EnableScheduling @EnableScheduling
@SpringBootApplication @SpringBootApplication(scanBasePackages = "cc.fascinated.bat")
@EnableMongock @EnableMongock
@Log4j2(topic = "Bat") @Log4j2(topic = "Bat")
public class BatApplication { public class BatApplication {

@ -1,5 +1,6 @@
package cc.fascinated.bat.command; package cc.fascinated.bat.command;
import cc.fascinated.bat.features.Feature;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
@ -38,6 +39,11 @@ public abstract class BatCommand implements BatCommandExecutor {
*/ */
private Category category; private Category category;
/**
* The feature that the command belongs to
*/
private Feature feature;
/** /**
* Whether the command can only be used by the bot owner * Whether the command can only be used by the bot owner
*/ */

@ -33,6 +33,7 @@ public abstract class Feature {
*/ */
public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) { public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) {
command.setCategory(category); command.setCategory(category);
command.setFeature(this);
commandService.registerCommand(command); commandService.registerCommand(command);
} }
} }

@ -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<String, Boolean> 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;
}
}

@ -21,6 +21,10 @@ public class AfkProfile extends Profile {
*/ */
private Map<String, String> afkUsers; private Map<String, String> afkUsers;
public AfkProfile() {
super("afk");
}
/** /**
* Adds a user to the AFK list * Adds a user to the AFK list
* *

@ -34,16 +34,13 @@ public class UserBirthday {
public int calculateAge() { public int calculateAge() {
Calendar birthdayCalendar = Calendar.getInstance(); Calendar birthdayCalendar = Calendar.getInstance();
birthdayCalendar.setTime(this.getBirthday()); birthdayCalendar.setTime(this.getBirthday());
Calendar today = Calendar.getInstance(); Calendar today = Calendar.getInstance();
int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR); int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR);
// Check if the birthday hasn't occurred yet this year // Check if the birthday hasn't occurred yet this year
if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) { if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) {
age--; age--;
} }
return age; return age;
} }
} }

@ -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();
}
}

@ -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();
}
}

@ -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));
}
}

@ -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();
}
}

@ -1,6 +1,7 @@
package cc.fascinated.bat.model; package cc.fascinated.bat.model;
import cc.fascinated.bat.common.ProfileHolder; 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.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.service.DiscordService; import cc.fascinated.bat.service.DiscordService;
import lombok.*; import lombok.*;
@ -75,6 +76,15 @@ public class BatGuild extends ProfileHolder {
return getProfile(NameHistoryProfile.class); return getProfile(NameHistoryProfile.class);
} }
/**
* Gets the feature profile
*
* @return the feature profile
*/
public FeatureProfile getFeatureProfile() {
return getProfile(FeatureProfile.class);
}
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
@Setter @Setter

@ -4,6 +4,7 @@ import cc.fascinated.bat.Consts;
import cc.fascinated.bat.command.*; import cc.fascinated.bat.command.*;
import cc.fascinated.bat.common.EmbedUtils; import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.config.Config; import cc.fascinated.bat.config.Config;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.model.BatGuild; import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser; import cc.fascinated.bat.model.BatUser;
import lombok.Getter; import lombok.Getter;
@ -81,9 +82,8 @@ public class CommandService extends ListenerAdapter {
return; return;
} }
jda.retrieveCommands().complete().forEach(command -> jda.deleteCommandById(command.getId()).complete()); jda.retrieveCommands().complete().forEach(command -> jda.deleteCommandById(command.getId()).complete());
adminGuild.updateCommands().addCommands(commands.values().stream() List<Command> registeredCommands = adminGuild.updateCommands().addCommands(commands.values().stream().map(BatCommand::getCommandData).toList()).complete();
.map(BatCommand::getCommandData).toList()).complete(); log.info("Registered {} slash commands in {}ms (DEV MODE)", registeredCommands.size(), System.currentTimeMillis() - before);
log.info("Registered {} slash commands in {}ms (DEV MODE)", commands.size(), System.currentTimeMillis() - before);
return; return;
} }
@ -123,7 +123,6 @@ public class CommandService extends ListenerAdapter {
} else { } else {
log.error("Unable to find the admin guild to register hidden commands"); log.error("Unable to find the admin guild to register hidden commands");
} }
log.info("Registered {} slash commands in {}ms", discordCommands.size(), System.currentTimeMillis() - before); log.info("Registered {} slash commands in {}ms", discordCommands.size(), System.currentTimeMillis() - before);
} }
@ -221,6 +220,16 @@ public class CommandService extends ListenerAdapter {
return; 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()); log.info("Executing command \"{}\" for user \"{}\"", commandName, user.getDiscordUser().getName());
executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(), executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(),
event.getMember(), event.getInteraction()); event.getMember(), event.getInteraction());

@ -2,6 +2,8 @@ package cc.fascinated.bat.service;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.features.Feature; import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.command.FeatureCommand;
import jakarta.annotation.PostConstruct;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@ -11,7 +13,9 @@ import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author Fascinated (fascinated7) * @author Fascinated (fascinated7)
@ -21,17 +25,28 @@ import java.util.List;
@Log4j2 @Log4j2
@DependsOn("commandService") @DependsOn("commandService")
public class FeatureService { public class FeatureService {
public static FeatureService INSTANCE;
private final ApplicationContext context;
private final CommandService commandService;
/** /**
* The registered features * The registered features
*/ */
private final List<Feature> features = new ArrayList<>(); private final Map<String, Feature> features = new HashMap<>();
@Autowired @Autowired
public FeatureService(@NonNull ApplicationContext context, @NonNull CommandService commandService) { public FeatureService(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
this.context = context;
this.commandService = commandService;
INSTANCE = this;
}
@PostConstruct
public void init() {
context.getBeansOfType(Feature.class) context.getBeansOfType(Feature.class)
.values() .values()
.forEach((feature) -> { .forEach((feature) -> {
features.add(context.getBean(feature.getClass())); features.put(feature.getName().toLowerCase(), feature);
}); });
context.getBeansOfType(BatCommand.class) context.getBeansOfType(BatCommand.class)
@ -40,6 +55,27 @@ public class FeatureService {
commandService.registerCommand(context.getBean(command.getClass())); commandService.registerCommand(context.getBean(command.getClass()));
}); });
commandService.registerCommand(context.getBean(FeatureCommand.class));
commandService.registerSlashCommands(); // Register all slash commands 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());
}
} }