[AB-242] refactoring suggestions

adding veto and unsuggest command
adding support for configuration whether or not a reply mentions the message
adding support to reply to a message via template
changed default mention config to exclude role mentions
This commit is contained in:
Sheldan
2021-04-29 01:00:27 +02:00
parent dec398c3f1
commit 020cc58c4a
38 changed files with 524 additions and 243 deletions

View File

@@ -9,10 +9,8 @@ import dev.sheldan.abstracto.core.command.config.ParameterValidator;
import dev.sheldan.abstracto.core.command.config.validator.MinIntegerValueValidator;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.ContextConverter;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,8 +34,7 @@ public class Accept extends AbstractConditionableCommand {
Long suggestionId = (Long) parameters.get(0);
String text = parameters.size() == 2 ? (String) parameters.get(1) : "";
log.debug("Using default reason for accept: {}.", parameters.size() != 2);
SuggestionLog suggestionModel = (SuggestionLog) ContextConverter.fromCommandContext(commandContext, SuggestionLog.class);
return suggestionService.acceptSuggestion(suggestionId, text, suggestionModel)
return suggestionService.acceptSuggestion(suggestionId, commandContext.getMessage(), text)
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -9,10 +9,8 @@ import dev.sheldan.abstracto.core.command.config.ParameterValidator;
import dev.sheldan.abstracto.core.command.config.validator.MinIntegerValueValidator;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.ContextConverter;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,8 +34,7 @@ public class Reject extends AbstractConditionableCommand {
Long suggestionId = (Long) parameters.get(0);
String text = parameters.size() == 2 ? (String) parameters.get(1) : "";
log.debug("Using default reason for accept: {}.", parameters.size() != 2);
SuggestionLog suggestionModel = (SuggestionLog) ContextConverter.fromCommandContext(commandContext, SuggestionLog.class);
return suggestionService.rejectSuggestion(suggestionId, text, suggestionModel)
return suggestionService.rejectSuggestion(suggestionId, commandContext.getMessage(), text)
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -7,10 +7,8 @@ import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.ContextConverter;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -29,9 +27,7 @@ public class Suggest extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
String text = (String) parameters.get(0);
SuggestionLog suggestLogModel = (SuggestionLog) ContextConverter.fromCommandContext(commandContext, SuggestionLog.class);
suggestLogModel.setSuggester(commandContext.getAuthor());
return suggestionService.createSuggestionMessage(commandContext.getAuthor(), text, suggestLogModel)
return suggestionService.createSuggestionMessage(commandContext.getMessage(), text)
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -0,0 +1,59 @@
package dev.sheldan.abstracto.suggestion.command;
import dev.sheldan.abstracto.core.command.UtilityModuleDefinition;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.config.ParameterValidator;
import dev.sheldan.abstracto.core.command.config.validator.MinIntegerValueValidator;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class UnSuggest extends AbstractConditionableCommand {
@Autowired
private SuggestionService suggestionService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Long suggestionId = (Long) parameters.get(0);
return suggestionService.removeSuggestion(suggestionId, commandContext.getAuthor())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
List<ParameterValidator> suggestionIdValidator = Arrays.asList(MinIntegerValueValidator.min(1L));
parameters.add(Parameter.builder().name("suggestionId").validators(suggestionIdValidator).type(Long.class).templated(true).build());
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("unSuggest")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.SUGGEST;
}
}

View File

@@ -0,0 +1,65 @@
package dev.sheldan.abstracto.suggestion.command;
import dev.sheldan.abstracto.core.command.UtilityModuleDefinition;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.config.ParameterValidator;
import dev.sheldan.abstracto.core.command.config.validator.MinIntegerValueValidator;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class Veto extends AbstractConditionableCommand {
@Autowired
private SuggestionService suggestionService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Long suggestionId = (Long) parameters.get(0);
String text = parameters.size() == 2 ? (String) parameters.get(1) : "";
log.debug("Using default reason for veto: {}.", parameters.size() != 2);
return suggestionService.vetoSuggestion(suggestionId, commandContext.getMessage(), text)
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
List<ParameterValidator> suggestionIdValidator = Arrays.asList(MinIntegerValueValidator.min(1L));
parameters.add(Parameter.builder().name("suggestionId").validators(suggestionIdValidator).type(Long.class).templated(true).build());
parameters.add(Parameter.builder().name("text").type(String.class).optional(true).remainder(true).templated(true).build());
HelpInfo helpInfo = HelpInfo.builder().templated(true).hasExample(true).build();
return CommandConfiguration.builder()
.name("veto")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.SUGGEST;
}
}

View File

@@ -0,0 +1,31 @@
package dev.sheldan.abstracto.suggestion.job;
import dev.sheldan.abstracto.suggestion.service.SuggestionService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
@Slf4j
@DisallowConcurrentExecution
@Component
@PersistJobDataAfterExecution
public class SuggestionCleanUpJob extends QuartzJobBean {
@Autowired
private SuggestionService suggestionService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Executing suggestion clean up job.");
try {
suggestionService.cleanUpSuggestions();
} catch (Exception exception) {
log.error("Suggestion clean up job failed.", exception);
}
}
}

View File

@@ -2,10 +2,15 @@ package dev.sheldan.abstracto.suggestion.repository;
import dev.sheldan.abstracto.core.models.ServerSpecificId;
import dev.sheldan.abstracto.suggestion.model.database.Suggestion;
import dev.sheldan.abstracto.suggestion.model.database.SuggestionState;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.List;
@Repository
public interface SuggestionRepository extends JpaRepository<Suggestion, ServerSpecificId> {
List<Suggestion> findByUpdatedLessThanAndStateNot(Instant start, SuggestionState state);
}

View File

@@ -6,34 +6,33 @@ import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.core.utils.MessageUtils;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.suggestion.config.SuggestionPostTarget;
import dev.sheldan.abstracto.suggestion.exception.SuggestionNotFoundException;
import dev.sheldan.abstracto.suggestion.exception.SuggestionUpdateException;
import dev.sheldan.abstracto.suggestion.exception.UnSuggestNotPossibleException;
import dev.sheldan.abstracto.suggestion.model.database.Suggestion;
import dev.sheldan.abstracto.suggestion.model.database.SuggestionState;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog;
import dev.sheldan.abstracto.suggestion.service.management.SuggestionManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class SuggestionServiceBean implements SuggestionService {
public static final String SUGGESTION_LOG_TEMPLATE = "suggest_log";
public static final String SUGGESTION_CREATION_TEMPLATE = "suggest_initial";
public static final String SUGGESTION_UPDATE_TEMPLATE = "suggest_update";
public static final String SUGGESTION_YES_EMOTE = "suggestionYes";
public static final String SUGGESTION_NO_EMOTE = "suggestionNo";
public static final String SUGGESTION_COUNTER_KEY = "suggestion";
@@ -68,68 +67,105 @@ public class SuggestionServiceBean implements SuggestionService {
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private MessageService messageService;
@Autowired
private UserService userService;
@Value("${abstracto.feature.suggestion.removalMaxAge}")
private Long removalMaxAgeSeconds;
@Value("${abstracto.feature.suggestion.removalDays}")
private Long autoRemovalMaxDays;
@Override
public CompletableFuture<Void> createSuggestionMessage(Member member, String text, SuggestionLog suggestionLog) {
AServer server = serverManagementService.loadServer(member.getGuild());
AUserInAServer suggester = userInServerManagementService.loadOrCreateUser(member);
public CompletableFuture<Void> createSuggestionMessage(Message commandMessage, String text) {
Member suggester = commandMessage.getMember();
Long serverId = suggester.getGuild().getIdLong();
AServer server = serverManagementService.loadServer(serverId);
AUserInAServer userSuggester = userInServerManagementService.loadOrCreateUser(suggester);
Long newSuggestionId = counterService.getNextCounterValue(server, SUGGESTION_COUNTER_KEY);
suggestionLog.setSuggestionId(newSuggestionId);
suggestionLog.setState(SuggestionState.NEW);
suggestionLog.setSuggesterUser(suggester);
suggestionLog.setText(text);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog, member.getGuild().getIdLong());
long guildId = member.getGuild().getIdLong();
log.info("Creating suggestion with id {} in server {} from member {}.", newSuggestionId, member.getGuild().getId(), member.getId());
List<CompletableFuture<Message>> completableFutures = postTargetService.sendEmbedInPostTarget(messageToSend, SuggestionPostTarget.SUGGESTION, guildId);
SuggestionLog model = SuggestionLog
.builder()
.suggestionId(newSuggestionId)
.state(SuggestionState.NEW)
.serverId(serverId)
.message(commandMessage)
.member(commandMessage.getMember())
.suggesterUser(userSuggester)
.suggester(suggester.getUser())
.text(text)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_CREATION_TEMPLATE, model, serverId);
log.info("Creating suggestion with id {} in server {} from member {}.", newSuggestionId, serverId, suggester.getIdLong());
List<CompletableFuture<Message>> completableFutures = postTargetService.sendEmbedInPostTarget(messageToSend, SuggestionPostTarget.SUGGESTION, serverId);
return FutureUtils.toSingleFutureGeneric(completableFutures).thenCompose(aVoid -> {
Message message = completableFutures.get(0).join();
log.debug("Posted message, adding reaction for suggestion {} to message {}.", newSuggestionId, message.getId());
CompletableFuture<Void> firstReaction = reactionService.addReactionToMessageAsync(SUGGESTION_YES_EMOTE, guildId, message);
CompletableFuture<Void> secondReaction = reactionService.addReactionToMessageAsync(SUGGESTION_NO_EMOTE, guildId, message);
CompletableFuture<Void> firstReaction = reactionService.addReactionToMessageAsync(SUGGESTION_YES_EMOTE, serverId, message);
CompletableFuture<Void> secondReaction = reactionService.addReactionToMessageAsync(SUGGESTION_NO_EMOTE, serverId, message);
return CompletableFuture.allOf(firstReaction, secondReaction).thenAccept(aVoid1 -> {
log.debug("Reaction added to message {} for suggestion {}.", message.getId(), newSuggestionId);
self.persistSuggestionInDatabase(member, text, message, newSuggestionId);
self.persistSuggestionInDatabase(suggester, text, message, newSuggestionId, commandMessage);
});
});
}
@Transactional
public void persistSuggestionInDatabase(Member member, String text, Message message, Long suggestionId) {
public void persistSuggestionInDatabase(Member member, String text, Message message, Long suggestionId, Message commandMessage) {
log.info("Persisting suggestion {} for server {} in database.", suggestionId, member.getGuild().getId());
suggestionManagementService.createSuggestion(member, text, message, suggestionId);
suggestionManagementService.createSuggestion(member, text, message, suggestionId, commandMessage);
}
@Override
public CompletableFuture<Void> acceptSuggestion(Long suggestionId, String text, SuggestionLog suggestionLog) {
Suggestion suggestion = suggestionManagementService.getSuggestion(suggestionId, suggestionLog.getGuild().getIdLong()).orElseThrow(() -> new SuggestionNotFoundException(suggestionId));
public CompletableFuture<Void> acceptSuggestion(Long suggestionId, Message commandMessage, String text) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.ACCEPTED);
log.info("Accepting suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(text, suggestionLog, suggestion);
return updateSuggestion(commandMessage.getMember(), text, suggestion);
}
private CompletableFuture<Void> updateSuggestion(String text, SuggestionLog suggestionLog, Suggestion suggestion) {
suggestionLog.setSuggesterUser(suggestion.getSuggester());
@Override
public CompletableFuture<Void> vetoSuggestion(Long suggestionId, Message commandMessage, String text) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.VETOED);
log.info("Vetoing suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(commandMessage.getMember(), text, suggestion);
}
private CompletableFuture<Void> updateSuggestion(Member memberExecutingCommand, String reason, Suggestion suggestion) {
Long serverId = suggestion.getServer().getId();
Long channelId = suggestion.getChannel().getId();
Long originalMessageId = suggestion.getMessageId();
Long serverId = suggestion.getServer().getId();
SuggestionLog model = SuggestionLog
.builder()
.suggestionId(suggestion.getSuggestionId().getId())
.state(suggestion.getState())
.suggesterUser(suggestion.getSuggester())
.serverId(serverId)
.member(memberExecutingCommand)
.originalMessageId(originalMessageId)
.text(suggestion.getSuggestionText())
.originalChannelId(channelId)
.reason(reason)
.build();
log.info("Updated posted suggestion {} in server {}.", suggestion.getSuggestionId().getId(), suggestion.getServer().getId());
suggestionLog.setOriginalChannelId(channelId);
suggestionLog.setOriginalMessageId(originalMessageId);
suggestionLog.setOriginalMessageUrl(MessageUtils.buildMessageUrl(serverId, channelId, originalMessageId));
AUserInAServer suggester = suggestion.getSuggester();
TextChannel textChannelById = channelService.getTextChannelFromServer(serverId, channelId);
CompletableFuture<Member> memberById = memberService.getMemberInServerAsync(serverId, suggester.getUserReference().getId());
suggestionLog.setState(suggestion.getState());
suggestionLog.setSuggestionId(suggestion.getSuggestionId().getId());
CompletableFuture<User> memberById = userService.retrieveUserForId(suggestion.getSuggester().getUserReference().getId());
CompletableFuture<Void> finalFuture = new CompletableFuture<>();
memberById.whenComplete((member, throwable) -> {
memberById.whenComplete((user, throwable) -> {
if(throwable == null) {
suggestionLog.setSuggester(member);
model.setSuggester(user);
}
channelService.retrieveMessageInChannel(textChannelById, originalMessageId).thenCompose(message ->
self.updateSuggestionMessageText(text, suggestionLog, message)
).thenAccept(aVoid -> finalFuture.complete(null));
self.updateSuggestionMessageText(reason, model).thenAccept(unused -> finalFuture.complete(null)).exceptionally(throwable1 -> {
finalFuture.completeExceptionally(throwable1);
return null;
});
}).exceptionally(throwable -> {
finalFuture.completeExceptionally(throwable);
return null;
});
return finalFuture;
@@ -137,27 +173,48 @@ public class SuggestionServiceBean implements SuggestionService {
}
@Transactional
public CompletableFuture<Void> updateSuggestionMessageText(String text, SuggestionLog suggestionLog, Message message) {
Optional<MessageEmbed> embedOptional = message.getEmbeds().stream().filter(embed -> embed.getDescription() != null).findFirst();
if(embedOptional.isPresent()) {
log.info("Updating the text of the suggestion {} in server {}.", suggestionLog.getSuggestionId(), message.getGuild().getId());
MessageEmbed suggestionEmbed = embedOptional.get();
suggestionLog.setReason(text);
suggestionLog.setText(suggestionEmbed.getDescription());
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog, suggestionLog.getGuild().getIdLong());
List<CompletableFuture<Message>> completableFutures = postTargetService.sendEmbedInPostTarget(messageToSend, SuggestionPostTarget.SUGGESTION, suggestionLog.getGuild().getIdLong());
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
} else {
log.warn("The message to update the suggestion for, did not contain an embed to update. Suggestions require an embed with a description as a container. MessageURL: {}", message.getJumpUrl());
throw new SuggestionUpdateException();
}
public CompletableFuture<Void> updateSuggestionMessageText(String text, SuggestionLog suggestionLog) {
suggestionLog.setReason(text);
Long serverId = suggestionLog.getServerId();
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_UPDATE_TEMPLATE, suggestionLog, serverId);
List<CompletableFuture<Message>> completableFutures = postTargetService.sendEmbedInPostTarget(messageToSend, SuggestionPostTarget.SUGGESTION, serverId);
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
}
@Override
public CompletableFuture<Void> rejectSuggestion(Long suggestionId, String text, SuggestionLog suggestionLog) {
Suggestion suggestion = suggestionManagementService.getSuggestion(suggestionId, suggestionLog.getGuild().getIdLong()).orElseThrow(() -> new SuggestionNotFoundException(suggestionId));
public CompletableFuture<Void> rejectSuggestion(Long suggestionId, Message commandMessage, String text) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.REJECTED);
log.info("Rejecting suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(text, suggestionLog, suggestion);
return updateSuggestion(commandMessage.getMember(), text, suggestion);
}
@Override
public CompletableFuture<Void> removeSuggestion(Long suggestionId, Member member) {
Long serverId = member.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
if(member.getIdLong() != suggestion.getSuggester().getUserReference().getId() ||
suggestion.getCreated().isBefore(Instant.now().minus(Duration.ofSeconds(removalMaxAgeSeconds)))) {
throw new UnSuggestNotPossibleException();
}
return messageService.deleteMessageInChannelInServer(suggestion.getServer().getId(), suggestion.getChannel().getId(), suggestion.getMessageId())
.thenAccept(unused -> self.deleteSuggestion(suggestionId, serverId));
}
@Override
@Transactional
public void cleanUpSuggestions() {
Instant pointInTime = Instant.now().minus(Duration.ofDays(autoRemovalMaxDays)).truncatedTo(ChronoUnit.DAYS);
List<Suggestion> suggestionsToRemove = suggestionManagementService.getSuggestionsUpdatedBeforeNotNew(pointInTime);
log.info("Removing {} suggestions older than {}.", suggestionsToRemove.size(), pointInTime);
suggestionsToRemove.forEach(suggestion -> log.info("Deleting suggestion {} in server {}.",
suggestion.getSuggestionId().getId(), suggestion.getSuggestionId().getServerId()));
suggestionManagementService.deleteSuggestion(suggestionsToRemove);
}
@Transactional
public void deleteSuggestion(Long suggestionId, Long serverId) {
suggestionManagementService.deleteSuggestion(serverId, suggestionId);
}
}

View File

@@ -6,6 +6,7 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.suggestion.exception.SuggestionNotFoundException;
import dev.sheldan.abstracto.suggestion.model.database.Suggestion;
import dev.sheldan.abstracto.suggestion.model.database.SuggestionState;
import dev.sheldan.abstracto.suggestion.repository.SuggestionRepository;
@@ -16,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@Component
@@ -35,35 +37,42 @@ public class SuggestionManagementServiceBean implements SuggestionManagementServ
private ServerManagementService serverManagementService;
@Override
public Suggestion createSuggestion(Member suggester, String text, Message message, Long suggestionId) {
public Suggestion createSuggestion(Member suggester, String text, Message message, Long suggestionId, Message commandMessage) {
AUserInAServer user = userInServerManagementService.loadOrCreateUser(suggester);
return this.createSuggestion(user, text, message, suggestionId);
return this.createSuggestion(user, text, message, suggestionId, commandMessage);
}
@Override
public Suggestion createSuggestion(AUserInAServer suggester, String text, Message message, Long suggestionId) {
long channelId = message.getChannel().getIdLong();
public Suggestion createSuggestion(AUserInAServer suggester, String text, Message createdMessage, Long suggestionId, Message commandMessage) {
long channelId = createdMessage.getChannel().getIdLong();
AChannel channel = channelManagementService.loadChannel(channelId);
AChannel commandChannel = channelManagementService.loadChannel(commandMessage.getChannel().getIdLong());
Suggestion suggestion = Suggestion
.builder()
.state(SuggestionState.NEW)
.suggester(suggester)
.suggestionText(text)
.suggestionId(new ServerSpecificId(suggester.getServerReference().getId(), suggestionId))
.server(suggester.getServerReference())
.suggestionDate(Instant.now())
.channel(channel)
.messageId(message.getIdLong())
.commandChannel(commandChannel)
.commandMessageId(commandMessage.getIdLong())
.messageId(createdMessage.getIdLong())
.build();
log.info("Persisting suggestion {} at message {} in channel {} on server {} from user {}.",
suggestionId, message.getId(), channelId, message.getGuild().getId(), suggester.getUserReference().getId());
suggestionId, createdMessage.getId(), channelId, createdMessage.getGuild().getId(), suggester.getUserReference().getId());
return suggestionRepository.save(suggestion);
}
@Override
public Optional<Suggestion> getSuggestion(Long suggestionId, Long serverId) {
public Optional<Suggestion> getSuggestionOptional(Long serverId, Long suggestionId) {
return suggestionRepository.findById(new ServerSpecificId(serverId, suggestionId));
}
@Override
public Suggestion getSuggestion(Long serverId, Long suggestionId) {
return getSuggestionOptional(serverId, suggestionId).orElseThrow(() -> new SuggestionNotFoundException(suggestionId));
}
@Override
public void setSuggestionState(Suggestion suggestion, SuggestionState newState) {
@@ -71,4 +80,25 @@ public class SuggestionManagementServiceBean implements SuggestionManagementServ
log.info("Setting suggestion {} in server {} to state {}.", suggestion.getSuggestionId().getId(), suggestion.getSuggestionId().getServerId(), newState);
suggestionRepository.save(suggestion);
}
@Override
public void deleteSuggestion(Long serverId, Long suggestionId) {
deleteSuggestion(getSuggestion(serverId, suggestionId));
}
@Override
public void deleteSuggestion(List<Suggestion> suggestions) {
suggestionRepository.deleteAll(suggestions);
}
@Override
public void deleteSuggestion(Suggestion suggestion) {
log.info("Deleting suggestion {} in server {}.", suggestion.getSuggestionId().getId(), suggestion.getSuggestionId().getServerId());
suggestionRepository.delete(suggestion);
}
@Override
public List<Suggestion> getSuggestionsUpdatedBeforeNotNew(Instant date) {
return suggestionRepository.findByUpdatedLessThanAndStateNot(date, SuggestionState.NEW);
}
}

View File

@@ -25,6 +25,16 @@
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${suggestionFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="unSuggest"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${suggestionFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="veto"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${suggestionFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -9,4 +9,5 @@
<include file="default_emote.xml" relativeToChangelogFile="true"/>
<include file="feature.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
<include file="suggestion_cleanup_job.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,19 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/pro ../../dbchangelog-3.8.xsd" >
<changeSet author="Sheldan" id="suggestion-cleanup-insert">
<insert tableName="scheduler_job">
<column name="name" value="suggestionCleanUpJob"/>
<column name="group_name" value="suggestion"/>
<column name="clazz" value="dev.sheldan.abstracto.suggestion.job.SuggestionCleanUpJob"/>
<column name="active" value="true"/>
<column name="cron_expression" value="0 0 0 * * ?"/>
<column name="recovery" value="false"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -17,9 +17,6 @@
<column name="state" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="suggestion_date" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="true"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="true"/>
</column>
@@ -33,11 +30,29 @@
<column name="suggester_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="suggestion_text" type="VARCHAR(2000)">
<constraints nullable="false"/>
</column>
<column name="command_message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="command_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="server_id, id" tableName="suggestion" constraintName="pk_suggestion" validate="true"/>
<addForeignKeyConstraint baseColumnNames="channel_id" baseTableName="suggestion" constraintName="fk_suggestion_channel" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="suggester_user_in_server_id" baseTableName="suggestion" constraintName="fk_suggestion_suggester" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id" referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="suggestion" constraintName="fk_suggestion_server" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="channel_id" baseTableName="suggestion" constraintName="fk_suggestion_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="command_channel_id" baseTableName="suggestion" constraintName="fk_suggestion_command_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="suggester_user_in_server_id" baseTableName="suggestion" constraintName="fk_suggestion_suggester"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="user_in_server_id" referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="suggestion" constraintName="fk_suggestion_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS suggestion_update_trigger ON suggestion;
CREATE TRIGGER suggestion_update_trigger BEFORE UPDATE ON suggestion FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
@@ -47,7 +62,7 @@
CREATE TRIGGER suggestion_insert_trigger BEFORE INSERT ON suggestion FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
<sql>
ALTER TABLE suggestion ADD CONSTRAINT check_suggestion_state CHECK (state IN ('NEW','ACCEPTED', 'REJECTED'));
ALTER TABLE suggestion ADD CONSTRAINT check_suggestion_state CHECK (state IN ('NEW','ACCEPTED', 'REJECTED', 'VETOED'));
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -3,3 +3,5 @@ abstracto.featureFlags.suggestion.enabled=false
abstracto.postTargets.suggestions.name=suggestions
abstracto.feature.suggestion.removalMaxAge=3600
abstracto.feature.suggestion.removalDays=2