Skip to content

Commit

Permalink
[AB-xxx] refactoring modmail to offer a feature mode to use threads i…
Browse files Browse the repository at this point in the history
…nstead of channels

adding various utilities for thread channels in core
fixing enable feature not showing validation messages
restructuring feature mode collection to be a startup listener, because postconstruct might not have the appropriate values available, and therefore not initialize the map correctly
  • Loading branch information
Sheldan committed Dec 24, 2023
1 parent 1f0bc49 commit befef1f
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 100 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.models.FeatureValidationResult;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.FeatureValidatorService;
import dev.sheldan.abstracto.core.service.GuildService;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition;
import dev.sheldan.abstracto.modmail.config.ModMailMode;
import dev.sheldan.abstracto.modmail.config.ModMailPostTargets;
import dev.sheldan.abstracto.modmail.model.template.ModMailCategoryValidationErrorModel;
import dev.sheldan.abstracto.modmail.model.template.ModMailThreadContainerValidationErrorModel;
import dev.sheldan.abstracto.modmail.service.ModMailThreadServiceBean;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer;
import net.dv8tion.jda.api.entities.channel.concrete.Category;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
* This component is used to validate whether or not the mod mail feature has a mod mail category configured, which points to
* This component is used to validate whether the mod mail feature has a mod mail category configured, which points to
* a category on the server it is configured for. This and other {@link dev.sheldan.abstracto.core.service.FeatureValidator}
* are used to fully validate the mod mail feature.
*/
Expand All @@ -34,8 +38,16 @@ public class ModMailFeatureValidatorBean implements ModMailFeatureValidator {
@Autowired
private FeatureValidatorService featureValidatorService;

@Autowired
private FeatureModeService featureModeService;

@Autowired
private PostTargetService postTargetService;

/**
* Checks if the mod mail category contains a value and whether or not this valid also points to a {@link Category}
* Checks if the mod mail category contains a value and whether this valid also points to a {@link Category}.
* Additionally, if the thread container feature mode is already enabled. This will check if the current posttarget
* for threads can hold threads.
* @param featureConfig The instance of {@link FeatureConfig} of mod mail
* @param server The {@link AServer} to check the config for
* @param validationResult The current {@link FeatureValidationResult} used to accumulate the wrong values
Expand All @@ -52,6 +64,29 @@ public void featureIsSetup(FeatureConfig featureConfig, AServer server, FeatureV
Long modMailCategory = configService.getLongValue(ModMailThreadServiceBean.MODMAIL_CATEGORY, server.getId());
validateModMailCategory(validationResult, guild, modMailCategory);
}
if (featureModeService.featureModeActive(ModMailFeatureDefinition.MOD_MAIL, server.getId(), ModMailMode.THREAD_CONTAINER)) {
Optional<GuildMessageChannel> modmailContainerOptional = postTargetService.getPostTargetChannel(ModMailPostTargets.MOD_MAIL_CONTAINER, server.getId());
if(modmailContainerOptional.isEmpty()) {
ModMailThreadContainerValidationErrorModel newError = ModMailThreadContainerValidationErrorModel
.builder()
.currentChannelId(null)
.build();
validationResult.getValidationErrorModels().add(newError);
validationResult.setValidationResult(false);
} else {
GuildMessageChannel threadContainer = modmailContainerOptional.get();
if(!(threadContainer instanceof IThreadContainer)) {
validationResult.setValidationResult(false);
ModMailThreadContainerValidationErrorModel newError = ModMailThreadContainerValidationErrorModel
.builder()
.currentChannelId(threadContainer.getIdLong())
.build();
validationResult.getValidationErrorModels().add(newError);
} else {
validationResult.setValidationResult(true);
}
}
}
}
}

Expand All @@ -70,6 +105,8 @@ public void validateModMailCategory(FeatureValidationResult validationResult, Gu
.currentCategoryId(modMailCategory)
.build();
validationResult.getValidationErrorModels().add(newError);
} else {
validationResult.setValidationResult(true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ abstracto.featureFlags.modmail.enabled=false

abstracto.postTargets.modmailLog.name=modmailLog
abstracto.postTargets.modmailPing.name=modmailPing
abstracto.postTargets.modmailContainer.name=modmailContainer

abstracto.featureModes.log.featureName=modmail
abstracto.featureModes.log.mode=log
abstracto.featureModes.log.enabled=true

abstracto.featureModes.threadContainer.featureName=modmail
abstracto.featureModes.threadContainer.mode=threadContainer
abstracto.featureModes.threadContainer.enabled=false

abstracto.featureModes.threadMessage.featureName=modmail
abstracto.featureModes.threadMessage.mode=threadMessage
abstracto.featureModes.threadMessage.enabled=true
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@Component
public class ModMailFeatureConfig implements FeatureConfig {

public static final String MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY = "modMailClosingText";
@Autowired
private ModMailFeatureValidator modMailFeatureValidator;

Expand All @@ -33,7 +34,7 @@ public FeatureDefinition getFeature() {

@Override
public List<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(ModMailPostTargets.MOD_MAIL_PING, ModMailPostTargets.MOD_MAIL_LOG);
return Arrays.asList(ModMailPostTargets.MOD_MAIL_PING, ModMailPostTargets.MOD_MAIL_LOG, ModMailPostTargets.MOD_MAIL_CONTAINER);
}

@Override
Expand All @@ -48,12 +49,12 @@ public List<String> getRequiredEmotes() {

@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(ModMailMode.LOGGING, ModMailMode.SEPARATE_MESSAGE);
return Arrays.asList(ModMailMode.LOGGING, ModMailMode.SEPARATE_MESSAGE, ModMailMode.THREAD_CONTAINER);
}

@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList("modMailClosingText");
return Arrays.asList(MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
@Getter
public enum ModMailMode implements FeatureMode {
LOGGING("log"), SEPARATE_MESSAGE("threadMessage");
LOGGING("log"), SEPARATE_MESSAGE("threadMessage"), THREAD_CONTAINER("threadContainer");

private final String key;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ public enum ModMailPostTargets implements PostTargetEnum {
/**
* The channel to be used to log the contents of closed mod mail threads
*/
MOD_MAIL_LOG("modmailLog");
MOD_MAIL_LOG("modmailLog"),
/**
* The thread used as a container for the modmail threads
*/
MOD_MAIL_CONTAINER("modmailContainer");

private String key;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ public Duration getDuration() {
private Boolean silently;
private User user;
private Long serverId;
private Long modmailThreadId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;

import java.util.List;

Expand All @@ -33,7 +33,7 @@ public class ModMailNotificationModel extends ServerContext {
*/
private List<ModMailRole> roles;
/**
* The {@link TextChannel} in which the mod mail thread is handled
* The {@link GuildMessageChannel} in which the mod mail thread is handled
*/
private TextChannel channel;
private GuildMessageChannel channel;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.modmail.model.template;

import dev.sheldan.abstracto.core.models.ValidationErrorModel;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
public class ModMailThreadContainerValidationErrorModel implements ValidationErrorModel {
private Long currentChannelId;

@Override
public String getTemplateName() {
return "feature_setup_modmail_thread_container_not_setup";
}

@Override
public Object getTemplateModel() {
return new Object();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,17 @@ public CompletableFuture<CommandResult> executeAsync(CommandContext commandConte
EnableFeatureResult result = enableFeature(serverId, featureKey);

if(result.featureDependencies.isEmpty()) {
return CompletableFuture.completedFuture(CommandResult.fromSuccess());
if(!result.validationResult.getValidationResult()) {
FeatureSwitchModel model = FeatureSwitchModel
.builder()
.validationText(result.validationResult.getValidationText())
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(ENABLE_FEATURE_RESPONSE_TEMPLATE_KEY, model, serverId);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(message -> CommandResult.fromIgnored());
} else {
return CompletableFuture.completedFuture(CommandResult.fromSuccess());
}
} else {
List<String> additionalFeatures = result.featureDependencies
.stream()
Expand All @@ -96,7 +106,11 @@ public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEven
String featureName = slashCommandParameterService.getCommandOption(FEATURE_NAME_PARAMETER, event, String.class);
EnableFeatureResult enableFeatureResult = enableFeature(event.getGuild().getIdLong(), featureName);
if(enableFeatureResult.featureDependencies.isEmpty()) {
return interactionService.replyEmbed(ENABLE_FEATURE_RESPONSE_TEMPLATE_KEY, event)
FeatureSwitchModel model = FeatureSwitchModel
.builder()
.validationText(enableFeatureResult.validationResult.getValidationText())
.build();
return interactionService.replyEmbed(ENABLE_FEATURE_RESPONSE_TEMPLATE_KEY, model, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
} else {
List<String> additionalFeatures = enableFeatureResult.featureDependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.concrete.Category;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
Expand Down Expand Up @@ -196,11 +197,8 @@ public CompletableFuture<Message> retrieveMessageInChannel(MessageChannel channe

@Override
public List<CompletableFuture<Message>> sendMessageToSendToChannel(MessageToSend messageToSend, MessageChannel textChannel) {
if(messageToSend.getEphemeral()) {
throw new IllegalArgumentException("Ephemeral messages are only supported in interaction context.");
}
if(textChannel instanceof GuildMessageChannel) {
GuildMessageChannel guildMessageChannel = (GuildMessageChannel) textChannel;
messageToSend.setEphemeral(false);
if(textChannel instanceof GuildMessageChannel guildMessageChannel) {
long maxFileSize = guildMessageChannel.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
messageToSend.getAttachedFiles().forEach(attachedFile -> {
Expand Down Expand Up @@ -427,18 +425,28 @@ public CompletableFuture<Message> removeComponents(MessageChannel channel, Long
return channel.editMessageComponentsById(messageId, new ArrayList<>()).submit();
}

@Override
public ThreadChannel getThreadChannel(Long threadChannelId) {
return botService.getInstance().getThreadChannelById(threadChannelId);
}

@Override
public CompletableFuture<Void> archiveThreadChannel(ThreadChannel threadChannel) {
return threadChannel.getManager().setArchived(true).submit();
}

@Override
public CompletableFuture<Void> deleteTextChannel(AChannel channel) {
return deleteTextChannel(channel.getServer().getId(), channel.getId());
}

@Override
public CompletableFuture<Void> deleteTextChannel(Long serverId, Long channelId) {
TextChannel textChannelById = botService.getInstance().getTextChannelById(channelId);
if(textChannelById != null) {
GuildChannel channelById = botService.getInstance().getGuildChannelById(channelId);
if(channelById != null) {
log.info("Deleting channel {} on server {}.", channelId, serverId);
metricService.incrementCounter(CHANNEL_DELETE_METRIC);
return textChannelById.delete().submit();
return channelById.delete().submit();
}
throw new ChannelNotInGuildException(channelId);
}
Expand Down Expand Up @@ -508,6 +516,21 @@ public CompletableFuture<TextChannel> createTextChannel(String name, AServer ser
throw new GuildNotFoundException(server.getId());
}

@Override
public CompletableFuture<ThreadChannel> createThread(TextChannel textChannel, String name) {
return textChannel.createThreadChannel(name).submit();
}

@Override
public CompletableFuture<ThreadChannel> createThreadWithStarterMessage(TextChannel textChannel, String name, Long messageId) {
return textChannel.createThreadChannel(name, messageId).submit();
}

@Override
public CompletableFuture<ThreadChannel> createPrivateThread(TextChannel textChannel, String name) {
return textChannel.createThreadChannel(name, true).submit();
}

@Override
public Optional<GuildChannel> getChannelFromAChannel(AChannel channel) {
return getGuildChannelFromServerOptional(channel.getServer().getId(), channel.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ public List<FeatureConfig> getAllFeatureConfigs() {

@Override
public FeatureConfig getFeatureDisplayForFeature(FeatureDefinition featureDefinition) {
Optional<FeatureConfig> any = getAllFeatureConfigs().stream().filter(featureDisplay -> featureDisplay.getFeature().equals(featureDefinition)).findAny();
if(any.isPresent()) {
return any.get();
List<FeatureConfig> allFeatureConfigs = getAllFeatureConfigs();
if(allFeatureConfigs != null) {
Optional<FeatureConfig> any = allFeatureConfigs.stream().filter(featureDisplay -> featureDisplay.getFeature().equals(featureDefinition)).findAny();
if(any.isPresent()) {
return any.get();
}
}
throw new FeatureNotFoundException(featureDefinition.getKey(), getFeaturesAsList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureMode;
import dev.sheldan.abstracto.core.exception.FeatureModeNotFoundException;
import dev.sheldan.abstracto.core.listener.AsyncStartupListener;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.property.FeatureModeProperty;
import dev.sheldan.abstracto.core.models.template.commands.AFeatureModeDisplay;
Expand All @@ -15,17 +16,18 @@
import dev.sheldan.abstracto.core.service.management.FeatureFlagManagementService;
import dev.sheldan.abstracto.core.service.management.FeatureModeManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

@Component
public class FeatureModeServiceBean implements FeatureModeService {
@Slf4j
public class FeatureModeServiceBean implements FeatureModeService, AsyncStartupListener {

@Autowired
private FeatureConfigService featureConfigService;
Expand Down Expand Up @@ -122,7 +124,10 @@ public FeatureMode getFeatureModeForKey(String featureKey, String featureModeKey
@Override
public List<FeatureMode> getAllAvailableFeatureModes() {
List<FeatureMode> fullFeatureModes = new ArrayList<>();
featureConfigService.getAllFeatureConfigs().forEach(featureConfig -> fullFeatureModes.addAll(featureConfig.getAvailableModes()));
List<FeatureConfig> allFeatureConfigs = featureConfigService.getAllFeatureConfigs();
if(allFeatureConfigs != null) {
allFeatureConfigs.forEach(featureConfig -> fullFeatureModes.addAll(featureConfig.getAvailableModes()));
}
return fullFeatureModes;
}

Expand Down Expand Up @@ -209,13 +214,16 @@ public boolean necessaryFeatureModesMet(FeatureAware featureAware, Long serverId
}


@PostConstruct
public void postConstruct() {
featureConfigService.getAllFeatureConfigs().forEach(featureConfig -> {
List<String> modes = new ArrayList<>();
featureConfig.getAvailableModes().forEach(featureMode -> modes.add(featureMode.getKey()));
featureModes.put(featureConfig.getFeature().getKey(), modes);
});
@Override
public void execute() {
List<FeatureConfig> allFeatureConfigs = featureConfigService.getAllFeatureConfigs();
log.info("Loading feature modes.");
if(allFeatureConfigs != null) {
allFeatureConfigs.forEach(featureConfig -> {
List<String> modes = new ArrayList<>();
featureConfig.getAvailableModes().forEach(featureMode -> modes.add(featureMode.getKey()));
featureModes.put(featureConfig.getFeature().getKey(), modes);
});
}
}

}
Loading

0 comments on commit befef1f

Please sign in to comment.