[AB-90] adding poll functionality

adding select menu functionality
not automatically acknowledging button interactions
adding ability to define positions for components
adding method to remove components to channel service
always replacing message contents with edit message in a channel
adding ability to reply a modal to a button interaction
moving post target specific methods from server management service to post target management
This commit is contained in:
Sheldan
2023-06-04 20:50:02 +02:00
parent efbcb5c84b
commit bac9832819
100 changed files with 3564 additions and 90 deletions

View File

@@ -127,7 +127,7 @@ public class StarboardServiceBean implements StarboardService {
public void persistPost(CachedMessage message, List<Long> userExceptAuthorIds, List<CompletableFuture<Message>> completableFutures, Long starboardChannelId, Long starredUserId, Long userReactingId) {
AUserInAServer innerStarredUser = userInServerManagementService.loadUserOptional(starredUserId).orElseThrow(() -> new UserInServerNotFoundException(starredUserId));
AChannel starboardChannel = channelManagementService.loadChannel(starboardChannelId);
Message starboardMessage = completableFutures.get(0).join();
Message starboardMessage = completableFutures.get(0).join(); // TODO null pointer if post target is disabled
AServerAChannelMessage aServerAChannelMessage = AServerAChannelMessage
.builder()
.messageId(starboardMessage.getIdLong())

View File

@@ -0,0 +1,91 @@
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.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionSlashCommandNames;
import dev.sheldan.abstracto.suggestion.service.PollService;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class CancelPoll extends AbstractConditionableCommand {
private static final String CANCEL_POLL_COMMAND = "cancelPoll";
private static final String POLL_ID_PARAMETER = "pollId";
private static final String CANCEL_POLL_RESPONSE = "cancelPoll_response";
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private InteractionService interactionService;
@Autowired
private PollService pollService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Long pollId = slashCommandParameterService.getCommandOption(POLL_ID_PARAMETER, event, Integer.class).longValue();
return pollService.cancelPoll(pollId, event.getGuild().getIdLong(), event.getMember())
.thenCompose(unused -> interactionService.replyEmbed(CANCEL_POLL_RESPONSE, event))
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public CommandConfiguration getConfiguration() {
List<ParameterValidator> pollIdValidator = Arrays.asList(MinIntegerValueValidator.min(1L));
Parameter pollIdParameter = Parameter
.builder()
.name(POLL_ID_PARAMETER)
.validators(pollIdValidator)
.type(Long.class)
.templated(true)
.build();
List<Parameter> parameters = Arrays.asList(pollIdParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(SuggestionSlashCommandNames.POLL_PUBLIC)
.commandName("cancel")
.build();
return CommandConfiguration.builder()
.name(CANCEL_POLL_COMMAND)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
}

View File

@@ -0,0 +1,107 @@
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.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionSlashCommandNames;
import dev.sheldan.abstracto.suggestion.service.PollService;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class ClosePoll extends AbstractConditionableCommand {
private static final String CLOSE_POLL_COMMAND = "closePoll";
private static final String POLL_ID_PARAMETER = "pollId";
private static final String TEXT_PARAMETER = "text";
private static final String CLOSE_POLL_RESPONSE = "closePoll_response";
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private InteractionService interactionService;
@Autowired
private PollService pollService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Long pollId = slashCommandParameterService.getCommandOption(POLL_ID_PARAMETER, event, Integer.class).longValue();
String text;
if(slashCommandParameterService.hasCommandOption(TEXT_PARAMETER, event)) {
text = slashCommandParameterService.getCommandOption(TEXT_PARAMETER, event, String.class);
} else {
text = "";
}
return pollService.closePoll(pollId, event.getGuild().getIdLong(), text, event.getMember())
.thenCompose(unused -> interactionService.replyEmbed(CLOSE_POLL_RESPONSE, event))
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public CommandConfiguration getConfiguration() {
List<ParameterValidator> pollIdValidator = Arrays.asList(MinIntegerValueValidator.min(1L));
Parameter pollIdParameter = Parameter
.builder()
.name(POLL_ID_PARAMETER)
.validators(pollIdValidator)
.type(Long.class)
.templated(true)
.build();
Parameter textParameter = Parameter
.builder()
.name(TEXT_PARAMETER)
.type(String.class)
.optional(true)
.remainder(true)
.templated(true)
.build();
List<Parameter> parameters = Arrays.asList(pollIdParameter, textParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(SuggestionSlashCommandNames.POLL)
.commandName("close")
.build();
return CommandConfiguration.builder()
.name(CLOSE_POLL_COMMAND)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
}

View File

@@ -0,0 +1,172 @@
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.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionSlashCommandNames;
import dev.sheldan.abstracto.suggestion.service.PollService;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Poll extends AbstractConditionableCommand {
private static final String POLL_COMMAND = "poll";
private static final String ALLOW_MULTIPLE_PARAMETER = "allowMultiple";
private static final String SHOW_DECISIONS_PARAMETER = "showDecisions";
private static final String ALLOW_ADDITIONS_PARAMETER = "allowAdditions";
private static final String POLL_DURATION_PARAMETER = "pollDuration";
private static final String POLL_DESCRIPTION_PARAMETER = "description";
private static final String POLL_OPTIONS_PARAMETER = "options";
private static final Integer OPTIONS_COUNT = 15;
private static final String POLL_RESPONSE_TEMPLATE_KEY = "poll_server_response";
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private InteractionService interactionService;
@Autowired
private PollService pollService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
List<String> options = new ArrayList<>();
for (int i = 0; i < OPTIONS_COUNT; i++) {
if(slashCommandParameterService.hasCommandOption(POLL_OPTIONS_PARAMETER + "_" + i, event)) {
String choice = slashCommandParameterService.getCommandOption(POLL_OPTIONS_PARAMETER + "_" + i, event, String.class);
options.add(choice);
}
}
Boolean allowMultiple = false;
if(slashCommandParameterService.hasCommandOption(ALLOW_MULTIPLE_PARAMETER, event)) {
allowMultiple = slashCommandParameterService.getCommandOption(ALLOW_MULTIPLE_PARAMETER, event, Boolean.class);
}
Boolean showDecisions = false;
if(slashCommandParameterService.hasCommandOption(SHOW_DECISIONS_PARAMETER, event)) {
showDecisions = slashCommandParameterService.getCommandOption(SHOW_DECISIONS_PARAMETER, event, Boolean.class);
}
Boolean allowAdditions = false;
if(slashCommandParameterService.hasCommandOption(ALLOW_ADDITIONS_PARAMETER, event)) {
allowAdditions = slashCommandParameterService.getCommandOption(ALLOW_ADDITIONS_PARAMETER, event, Boolean.class);
}
Duration pollDuration = null;
if(slashCommandParameterService.hasCommandOption(POLL_DURATION_PARAMETER, event)) {
String durationString = slashCommandParameterService.getCommandOption(POLL_DURATION_PARAMETER, event, Duration.class, String.class);
pollDuration = ParseUtils.parseDuration(durationString);
}
Boolean actualMultiple = allowMultiple;
Boolean actualDecisions = showDecisions;
Boolean actualAdditions = allowAdditions;
Duration actualDuration = pollDuration;
String description = slashCommandParameterService.getCommandOption(POLL_DESCRIPTION_PARAMETER, event, String.class);
return event.deferReply()
.submit()
.thenCompose(interactionHook -> pollService.createServerPoll(event.getMember(), options, description, actualMultiple, actualAdditions, actualDecisions, actualDuration)
.thenAccept(unused -> interactionService.sendMessageToInteraction(POLL_RESPONSE_TEMPLATE_KEY, new Object(), interactionHook)))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter allowMultipleParameter = Parameter
.builder()
.name(ALLOW_MULTIPLE_PARAMETER)
.type(Boolean.class)
.templated(true)
.optional(true)
.build();
Parameter showDecisions = Parameter
.builder()
.name(SHOW_DECISIONS_PARAMETER)
.type(Boolean.class)
.templated(true)
.optional(true)
.build();
Parameter allowAdditions = Parameter
.builder()
.name(ALLOW_ADDITIONS_PARAMETER)
.type(Boolean.class)
.templated(true)
.optional(true)
.build();
Parameter description = Parameter
.builder()
.name(POLL_DESCRIPTION_PARAMETER)
.type(String.class)
.templated(true)
.build();
Parameter duration = Parameter
.builder()
.name(POLL_DURATION_PARAMETER)
.type(Duration.class)
.templated(true)
.optional(true)
.build();
Parameter optionsParameter = Parameter
.builder()
.name(POLL_OPTIONS_PARAMETER)
.type(String.class)
.templated(true)
.remainder(true)
.listSize(OPTIONS_COUNT)
.isListParam(true)
.build();
List<Parameter> parameters = Arrays.asList(description, optionsParameter, allowMultipleParameter, showDecisions, allowAdditions, duration);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(SuggestionSlashCommandNames.POLL_PUBLIC)
.commandName("server")
.build();
return CommandConfiguration.builder()
.name(POLL_COMMAND)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
}

View File

@@ -0,0 +1,157 @@
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.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionSlashCommandNames;
import dev.sheldan.abstracto.suggestion.service.PollService;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class QuickPoll extends AbstractConditionableCommand {
private static final String POLL_COMMAND = "quickPoll";
private static final String ALLOW_MULTIPLE_PARAMETER = "allowMultiple";
private static final String POLL_DURATION_PARAMETER = "pollDuration";
private static final String SHOW_DECISIONS_PARAMETER = "showDecisions";
private static final String POLL_DESCRIPTION_PARAMETER = "description";
private static final String POLL_OPTIONS_PARAMETER = "options";
private static final Integer OPTIONS_COUNT = 15;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private PollService pollService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
List<String> options = new ArrayList<>();
for (int i = 0; i < OPTIONS_COUNT; i++) {
if(slashCommandParameterService.hasCommandOption(POLL_OPTIONS_PARAMETER + "_" + i, event)) {
String choice = slashCommandParameterService.getCommandOption(POLL_OPTIONS_PARAMETER + "_" + i, event, String.class);
options.add(choice);
}
}
Boolean allowMultiple = false;
if(slashCommandParameterService.hasCommandOption(ALLOW_MULTIPLE_PARAMETER, event)) {
allowMultiple = slashCommandParameterService.getCommandOption(ALLOW_MULTIPLE_PARAMETER, event, Boolean.class);
}
Boolean showDecisions = false;
if(slashCommandParameterService.hasCommandOption(SHOW_DECISIONS_PARAMETER, event)) {
showDecisions = slashCommandParameterService.getCommandOption(SHOW_DECISIONS_PARAMETER, event, Boolean.class);
}
Duration pollDuration = null;
if(slashCommandParameterService.hasCommandOption(POLL_DURATION_PARAMETER, event)) {
String durationString = slashCommandParameterService.getCommandOption(POLL_DURATION_PARAMETER, event, Duration.class, String.class);
pollDuration = ParseUtils.parseDuration(durationString);
}
Boolean actualMultiple = allowMultiple;
Boolean actualShowDecisions = showDecisions;
Duration actualDuration = pollDuration;
String description = slashCommandParameterService.getCommandOption(POLL_DESCRIPTION_PARAMETER, event, String.class);
return event.deferReply()
.submit()
.thenCompose(interactionHook -> pollService.createQuickPoll(event.getMember(), options, description, actualMultiple, actualShowDecisions, interactionHook, actualDuration))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter allowMultipleParameter = Parameter
.builder()
.name(ALLOW_MULTIPLE_PARAMETER)
.type(Boolean.class)
.templated(true)
.optional(true)
.build();
Parameter description = Parameter
.builder()
.name(POLL_DESCRIPTION_PARAMETER)
.type(String.class)
.templated(true)
.build();
Parameter showDecisions = Parameter
.builder()
.name(SHOW_DECISIONS_PARAMETER)
.type(Boolean.class)
.templated(true)
.optional(true)
.build();
Parameter duration = Parameter
.builder()
.name(POLL_DURATION_PARAMETER)
.type(Duration.class)
.templated(true)
.optional(true)
.build();
Parameter optionsParameter = Parameter
.builder()
.name(POLL_OPTIONS_PARAMETER)
.type(String.class)
.templated(true)
.remainder(true)
.listSize(OPTIONS_COUNT)
.isListParam(true)
.build();
List<Parameter> parameters = Arrays.asList(description, optionsParameter, allowMultipleParameter, showDecisions, duration);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(SuggestionSlashCommandNames.POLL_PUBLIC)
.commandName("quick")
.build();
return CommandConfiguration.builder()
.name(POLL_COMMAND)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
}

View File

@@ -0,0 +1,43 @@
package dev.sheldan.abstracto.suggestion.job;
import dev.sheldan.abstracto.suggestion.service.PollService;
import lombok.Getter;
import lombok.Setter;
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
@Getter
@Setter
public class QuickPollEvaluationJob extends QuartzJobBean {
private Long pollId;
private Long serverId;
@Autowired
private PollService pollService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Executing poll evaluation job for quick poll {} in server {}.", pollId, serverId);
try {
pollService.evaluateQuickPoll(pollId, serverId).thenAccept(unused -> {
log.info("Evaluated quick poll {} in server {}.", pollId, serverId);
}).exceptionally(throwable -> {
log.error("Failed to evaluate quick poll {} in server {}.", pollId, serverId, throwable);
return null;
});
} catch (Exception exception) {
log.error("Quick poll evaluation job failed.", exception);
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.sheldan.abstracto.suggestion.job;
import dev.sheldan.abstracto.suggestion.service.PollService;
import lombok.Getter;
import lombok.Setter;
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
@Getter
@Setter
public class ServerPollEvaluationJob extends QuartzJobBean {
private Long pollId;
private Long serverId;
@Autowired
private PollService pollService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Executing poll evaluation job for server poll {} in server {}.", pollId, serverId);
try {
pollService.evaluateServerPoll(pollId, serverId).thenAccept(unused -> {
log.info("Evaluated server poll {} in server {}.", pollId, serverId);
}).exceptionally(throwable -> {
log.error("Failed to evaluate server poll {} in server {}.", pollId, serverId, throwable);
return null;
});
} catch (Exception exception) {
log.error("Server poll evaluation job failed.", exception);
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.sheldan.abstracto.suggestion.job;
import dev.sheldan.abstracto.suggestion.service.PollService;
import lombok.Getter;
import lombok.Setter;
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
@Getter
@Setter
public class ServerPollReminderJob extends QuartzJobBean {
private Long pollId;
private Long serverId;
@Autowired
private PollService pollService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Executing server poll reminder job for server poll {} in server {}.", pollId, serverId);
try {
pollService.remindServerPoll(pollId, serverId).thenAccept(unused -> {
log.info("Evaluated server poll {} in server {}.", pollId, serverId);
}).exceptionally(throwable -> {
log.error("Failed to evaluate server poll {} in server {}.", pollId, serverId, throwable);
return null;
});
} catch (Exception exception) {
log.error("Server poll evaluation job failed.", exception);
}
}
}

View File

@@ -0,0 +1,71 @@
package dev.sheldan.abstracto.suggestion.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListener;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerModel;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerResult;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import dev.sheldan.abstracto.suggestion.model.payload.QuickPollSelectionMenuPayload;
import dev.sheldan.abstracto.suggestion.model.template.PollDecisionNotificationModel;
import dev.sheldan.abstracto.suggestion.service.PollService;
import dev.sheldan.abstracto.suggestion.service.PollServiceBean;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class QuickPollDecisionListener implements StringSelectMenuListener {
@Autowired
private PollService pollService;
@Autowired
private InteractionService interactionService;
private static final String POLL_DECISION_NOTIFICATION = "poll_quick_decision_notification";
@Override
public StringSelectMenuListenerResult execute(StringSelectMenuListenerModel model) {
StringSelectInteractionEvent event = model.getEvent();
QuickPollSelectionMenuPayload payload = (QuickPollSelectionMenuPayload) model.getDeserializedPayload();
PollDecisionNotificationModel notificationModel = PollDecisionNotificationModel
.builder()
.chosenValues(event.getValues())
.pollId(payload.getPollId())
.memberNameDisplay(MemberNameDisplay.fromMember(event.getMember()))
.serverId(model.getServerId())
.build();
pollService.setDecisionsInPollTo(event.getMember(), event.getValues(), payload.getPollId(), PollType.QUICK)
.thenCompose(unused -> FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(POLL_DECISION_NOTIFICATION, notificationModel, event.getInteraction().getHook())))
.exceptionally(throwable -> {
log.info("Failed to member {} in server {} about decision in poll {}.", event.getMember().getIdLong(), model.getServerId(), payload.getPollId(), throwable);
return null;
}).thenAccept(unused1 -> {
log.info("Notified member {} in server {} about decision in poll {}.", event.getMember().getIdLong(), model.getServerId(), payload.getPollId());
});
return StringSelectMenuListenerResult.ACKNOWLEDGED;
}
@Override
public Boolean handlesEvent(StringSelectMenuListenerModel model) {
return model.getOrigin().equals(PollServiceBean.QUICK_POLL_SELECTION_MENU_ORIGIN);
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -0,0 +1,104 @@
package dev.sheldan.abstracto.suggestion.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.interaction.modal.ModalService;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.payload.PollAddOptionButtonPayload;
import dev.sheldan.abstracto.suggestion.model.template.PollAddOptionModalModel;
import dev.sheldan.abstracto.suggestion.model.payload.PollAddOptionModalPayload;
import dev.sheldan.abstracto.suggestion.service.PollServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j
public class ServerPollAddOptionButtonListener implements ButtonClickedListener {
@Autowired
private ComponentService componentService;
@Autowired
private ModalService modalService;
@Autowired
private ServerPollAddOptionButtonListener self;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
private static final String SERVER_POLL_ADD_OPTION_MODAL_TEMPLATE = "poll_add_option";
public static final String SERVER_POLL_ADD_OPTION_MODAL_ORIGIN = "SERVER_POLL_ADD_OPTION_MODAL";
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
PollAddOptionButtonPayload payload = (PollAddOptionButtonPayload) model.getDeserializedPayload();
String modalId = componentService.generateComponentId();
String labelInputId = componentService.generateComponentId();
String descriptionInputId = componentService.generateComponentId();
PollAddOptionModalModel modalModel = PollAddOptionModalModel
.builder()
.descriptionInputComponentId(descriptionInputId)
.modalId(modalId)
.labelInputComponentId(labelInputId)
.build();
modalService.replyModal(model.getEvent(), SERVER_POLL_ADD_OPTION_MODAL_TEMPLATE, modalModel).thenAccept(unused -> {
log.info("Opened a model for entering a new option for poll {} in server {} for user {}.",
payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong());
self.persistModalPayload(modalModel, model.getServerId(), payload.getPollId());
}).exceptionally(throwable -> {
log.error("Failed to show modal for entering a new option for poll {} in server {} for user {}.",
payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong(), throwable);
return null;
});
return ButtonClickedListenerResult.ACKNOWLEDGED;
}
@Transactional
public void persistModalPayload(PollAddOptionModalModel model, Long serverId, Long pollId) {
PollAddOptionModalPayload payload = PollAddOptionModalPayload
.builder()
.modalId(model.getModalId())
.labelInputComponentId(model.getLabelInputComponentId())
.descriptionInputComponentId(model.getDescriptionInputComponentId())
.serverId(serverId)
.pollId(pollId)
.build();
ModalConfigPayload payloadConfig = ModalConfigPayload
.builder()
.modalPayload(payload)
.origin(SERVER_POLL_ADD_OPTION_MODAL_ORIGIN)
.payloadType(payload.getClass())
.modalId(model.getModalId())
.build();
componentPayloadManagementService.createModalPayload(payloadConfig, serverId);
}
@Override
public Boolean autoAcknowledgeEvent() {
return false;
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
return PollServiceBean.SERVER_POLL_ADD_OPTION_ORIGIN.equals(model.getOrigin());
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -0,0 +1,120 @@
package dev.sheldan.abstracto.suggestion.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListener;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerModel;
import dev.sheldan.abstracto.core.interaction.modal.listener.ModalInteractionListenerResult;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.exception.PollOptionAlreadyExistsException;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import dev.sheldan.abstracto.suggestion.model.payload.PollAddOptionModalPayload;
import dev.sheldan.abstracto.suggestion.model.template.PollAddOptionNotificationModel;
import dev.sheldan.abstracto.suggestion.service.PollService;
import dev.sheldan.abstracto.suggestion.service.management.PollManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j
public class ServerPollAddOptionModalListener implements ModalInteractionListener {
@Autowired
private PollService pollService;
@Autowired
private ServerPollAddOptionModalListener self;
@Autowired
private InteractionService interactionService;
@Autowired
private PollManagementService pollManagementService;
private static final String POLL_ADD_OPTION_NOTIFICATION = "poll_add_option_notification";
@Override
public ModalInteractionListenerResult execute(ModalInteractionListenerModel model) {
PollAddOptionModalPayload payload = (PollAddOptionModalPayload) model.getDeserializedPayload();
log.info("Handling modal event to add options to poll {} in server {} by member {}.", payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong());
String labelContent = model
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getLabelInputComponentId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
Poll affectedPoll = pollManagementService.getPollByPollId(payload.getPollId(), payload.getServerId(), PollType.STANDARD);
if(affectedPoll.getOptions().stream().anyMatch(pollOption -> pollOption.getLabel().equals(labelContent))) {
throw new PollOptionAlreadyExistsException();
}
String descriptionContent = model
.getEvent()
.getValues()
.stream()
.filter(modalMapping -> modalMapping.getId().equals(payload.getDescriptionInputComponentId()))
.map(ModalMapping::getAsString)
.findFirst()
.orElse(null);
PollAddOptionNotificationModel pollAddOptionNotificationModel = PollAddOptionNotificationModel
.builder()
.description(descriptionContent)
.memberNameDisplay(MemberNameDisplay.fromMember(model.getEvent().getMember()))
.label(labelContent)
.value(labelContent)
.pollId(payload.getPollId())
.serverId(payload.getServerId())
.build();
model.getEvent().deferReply(true).queue(interactionHook -> {
self.updatePoll(model, payload, labelContent, descriptionContent);
FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(POLL_ADD_OPTION_NOTIFICATION, pollAddOptionNotificationModel, model.getEvent().getInteraction().getHook())).thenAccept(unused -> {
log.info("Send notification about successfully adding option to poll {} in server {} to member {}", payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong());
}).exceptionally(throwable -> {
log.info("Failed to send notification about adding option to poll {} in server {} to member {}", payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong());
return null;
});
}, throwable -> {
log.error("Failed to acknowledge modal interaction for poll add option modal listener in guild {}.", model.getServerId(), throwable);
});
return ModalInteractionListenerResult.ACKNOWLEDGED;
}
@Transactional
public void updatePoll(ModalInteractionListenerModel model, PollAddOptionModalPayload payload, String labelContent, String descriptionContent) {
pollService.addOptionToServerPoll(payload.getPollId(), payload.getServerId(), model.getEvent().getMember(), labelContent, descriptionContent).thenAccept(unused -> {
log.info("Added option to poll {} in server {} by member {}.", payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong());
}).exceptionally(throwable -> {
log.error("Failed to add option to poll {} in server {} by member {}.",
payload.getPollId(), payload.getServerId(), model.getEvent().getMember().getIdLong(), throwable);
return null;
});
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean handlesEvent(ModalInteractionListenerModel model) {
return ServerPollAddOptionButtonListener.SERVER_POLL_ADD_OPTION_MODAL_ORIGIN.equals(model.getOrigin());
}
}

View File

@@ -0,0 +1,71 @@
package dev.sheldan.abstracto.suggestion.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListener;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerModel;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerResult;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import dev.sheldan.abstracto.suggestion.model.template.PollDecisionNotificationModel;
import dev.sheldan.abstracto.suggestion.model.payload.ServerPollSelectionMenuPayload;
import dev.sheldan.abstracto.suggestion.service.PollService;
import dev.sheldan.abstracto.suggestion.service.PollServiceBean;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ServerPollDecisionListener implements StringSelectMenuListener {
@Autowired
private PollService pollService;
@Autowired
private InteractionService interactionService;
private static final String POLL_DECISION_NOTIFICATION = "poll_decision_notification";
@Override
public StringSelectMenuListenerResult execute(StringSelectMenuListenerModel model) {
StringSelectInteractionEvent event = model.getEvent();
ServerPollSelectionMenuPayload payload = (ServerPollSelectionMenuPayload) model.getDeserializedPayload();
PollDecisionNotificationModel notificationModel = PollDecisionNotificationModel
.builder()
.chosenValues(event.getValues())
.pollId(payload.getPollId())
.memberNameDisplay(MemberNameDisplay.fromMember(event.getMember()))
.serverId(model.getServerId())
.build();
pollService.setDecisionsInPollTo(event.getMember(), event.getValues(), payload.getPollId(), PollType.STANDARD)
.thenCompose(unused -> FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(POLL_DECISION_NOTIFICATION, notificationModel, event.getInteraction().getHook())))
.exceptionally(throwable -> {
log.info("Failed to member {} in server {} about decision in poll {}.", event.getMember().getIdLong(), model.getServerId(), payload.getPollId(), throwable);
return null;
}).thenAccept(unused1 -> {
log.info("Notified member {} in server {} about decision in poll {}.", event.getMember().getIdLong(), model.getServerId(), payload.getPollId());
});
return StringSelectMenuListenerResult.ACKNOWLEDGED;
}
@Override
public Boolean handlesEvent(StringSelectMenuListenerModel model) {
return model.getOrigin().equals(PollServiceBean.SERVER_POLL_SELECTION_MENU_ORIGIN);
}
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.suggestion.repository;
import dev.sheldan.abstracto.suggestion.model.database.PollOption;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PollOptionRepository extends JpaRepository<PollOption, Long> {
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.suggestion.repository;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PollRepository extends JpaRepository<Poll, Long> {
Optional<Poll> findByPollIdAndServer_IdAndType(Long pollId, Long serverId, PollType pollType);
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.suggestion.repository;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecisionOption;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PollUserDecisionOptionRepository extends JpaRepository<PollUserDecisionOption, Long> {
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.suggestion.repository;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecision;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PollUserDecisionRepository extends JpaRepository<PollUserDecision, Long> {
Optional<PollUserDecision> findPollUserDecisionByPollAndVoter(Poll poll, AUserInAServer voter);
}

View File

@@ -0,0 +1,567 @@
package dev.sheldan.abstracto.suggestion.service;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.button.ButtonConfigModel;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuConfigModel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.core.utils.MessageUtils;
import dev.sheldan.abstracto.scheduling.model.JobParameters;
import dev.sheldan.abstracto.scheduling.service.SchedulerService;
import dev.sheldan.abstracto.suggestion.config.PollFeatureMode;
import dev.sheldan.abstracto.suggestion.config.PollPostTarget;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.exception.PollCancellationNotPossibleException;
import dev.sheldan.abstracto.suggestion.exception.PollOptionAlreadyExistsException;
import dev.sheldan.abstracto.suggestion.model.payload.PollAddOptionButtonPayload;
import dev.sheldan.abstracto.suggestion.model.PollCreationRequest;
import dev.sheldan.abstracto.suggestion.model.database.*;
import dev.sheldan.abstracto.suggestion.model.payload.QuickPollSelectionMenuPayload;
import dev.sheldan.abstracto.suggestion.model.template.*;
import dev.sheldan.abstracto.suggestion.model.payload.ServerPollSelectionMenuPayload;
import dev.sheldan.abstracto.suggestion.service.management.PollManagementService;
import dev.sheldan.abstracto.suggestion.service.management.PollOptionManagementService;
import dev.sheldan.abstracto.suggestion.service.management.PollUserDecisionManagementService;
import dev.sheldan.abstracto.suggestion.service.management.PollUserDecisionOptionManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.InteractionHook;
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.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component
@Slf4j
public class PollServiceBean implements PollService {
@Autowired
private CounterService counterService;
@Autowired
private PollManagementService pollManagementService;
@Autowired
private PollOptionManagementService pollOptionManagementService;
@Autowired
private PostTargetService postTargetService;
@Autowired
private TemplateService templateService;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private PollUserDecisionManagementService pollUserDecisionManagementService;
@Autowired
private PollUserDecisionOptionManagementService pollUserDecisionOptionManagementService;
@Autowired
private ChannelService channelService;
@Autowired
private InteractionService interactionService;
@Autowired
private SchedulerService schedulerService;
@Autowired
private ConfigService configService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private MessageService messageService;
@Autowired
private PollServiceBean self;
private static final String POLLS_COUNTER_KEY = "POLLS";
public static final String SERVER_POLL_SELECTION_MENU_ORIGIN = "SERVER_POLL_SELECTION_MENU";
public static final String SERVER_POLL_ADD_OPTION_ORIGIN = "SERVER_POLL_ADD_OPTION_BUTTON";
private static final String SERVER_POLL_TEMPLATE_KEY = "poll_server_message";
private static final String SERVER_POLL_CLOSE_MESSAGE = "poll_server_close_message";
private static final String SERVER_POLL_REMINDER_TEMPLATE_KEY = "poll_server_reminder_message";
private static final String SERVER_POLL_EVALUATION_UPDATE_TEMPLATE_KEY = "poll_server_evaluation_update_message";
private static final String QUICK_POLLS_COUNTER_KEY = "QUICK_POLLS";
public static final String QUICK_POLL_SELECTION_MENU_ORIGIN = "QUICK_POLL_SELECTION_MENU";
private static final String QUICK_POLL_TEMPLATE_KEY = "poll_quick_message";
private static final String QUICK_POLL_EVALUATION_UPDATE_TEMPLATE_KEY = "poll_quick_evaluation_update_message";
@Value("${abstracto.feature.poll.removalMaxAge}")
private Long removalMaxAgeSeconds;
@Override
@Transactional
public CompletableFuture<Void> createServerPoll(Member creator, List<String> options, String description,
Boolean allowMultiple, Boolean allowAddition, Boolean showDecisions, Duration pollDuration) {
Long serverId = creator.getGuild().getIdLong();
HashSet<String> optionAsSet = new HashSet<>(options);
if(optionAsSet.size() != options.size()) {
throw new PollOptionAlreadyExistsException();
}
Long pollId = counterService.getNextCounterValue(serverId, POLLS_COUNTER_KEY);
log.info("Creating server poll {} in server {} because of user {}.", pollId, serverId, creator.getIdLong());
List<PollMessageOption> parsedOptions = parseOptions(options);
String selectionMenuId = componentService.generateComponentId();
String addOptionButtonId = componentService.generateComponentId();
if(pollDuration == null) {
Long pollDurationSeconds = configService.getLongValueOrConfigDefault(PollService.SERVER_POLL_DURATION_SECONDS, serverId);
log.info("No duration provided - using {} seconds from configuration.", pollDurationSeconds);
pollDuration = Duration.ofSeconds(pollDurationSeconds);
}
Instant targetDate = Instant.now().plus(pollDuration);
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("serverId", serverId.toString());
parameters.put("pollId", pollId.toString());
JobParameters jobParameters = JobParameters.builder().parameters(parameters).build();
String triggerKey = null;
if(featureModeService.featureModeActive(SuggestionFeatureDefinition.POLL, serverId, PollFeatureMode.POLL_AUTO_EVALUATE)) {
log.info("Creating scheduled job to evaluate poll {} in server {} at {}.", pollId, serverId, targetDate);
triggerKey = schedulerService.executeJobWithParametersOnce("serverPollEvaluationJob", "poll", jobParameters, Date.from(targetDate));
}
String reminderTriggerKey = null;
if(featureModeService.featureModeActive(SuggestionFeatureDefinition.POLL, serverId, PollFeatureMode.POLL_REMINDER)) {
log.info("Creating scheduled job to remind about poll {} in server {} at {}.", pollId, serverId, targetDate);
reminderTriggerKey = schedulerService.executeJobWithParametersOnce("serverPollReminderJob", "poll", jobParameters, Date.from(targetDate));
}
PollCreationRequest pollCreationRequest = PollCreationRequest
.builder()
.pollId(pollId)
.type(PollType.STANDARD)
.allowAddition(allowAddition)
.allowMultiple(allowMultiple)
.showDecisions(showDecisions)
.addOptionButtonId(addOptionButtonId)
.reminderJobTrigger(reminderTriggerKey)
.selectionMenuId(selectionMenuId)
.serverId(serverId)
.evaluationJobTrigger(triggerKey)
.targetDate(targetDate)
.creatorId(creator.getIdLong())
.description(description)
.options(parsedOptions)
.build();
ServerPollMessageModel model = ServerPollMessageModel
.builder()
.creator(MemberDisplay.fromMember(creator))
.description(description)
.pollId(pollId)
.state(PollState.NEW)
.allowMultiple(allowMultiple)
.showDecisions(showDecisions)
.allowAdditions(allowAddition)
.endDate(targetDate)
.options(parsedOptions)
.addOptionButtonId(addOptionButtonId)
.selectionMenuId(selectionMenuId)
.build();
ServerPollSelectionMenuPayload payload = ServerPollSelectionMenuPayload
.builder()
.serverId(serverId)
.pollId(pollId)
.build();
SelectMenuConfigModel selectMenuConfigModel = SelectMenuConfigModel
.builder()
.selectMenuId(selectionMenuId)
.origin(SERVER_POLL_SELECTION_MENU_ORIGIN)
.selectMenuPayload(payload)
.payloadType(ServerPollSelectionMenuPayload.class)
.build();
componentPayloadManagementService.createStringSelectMenuPayload(selectMenuConfigModel, serverId);
PollAddOptionButtonPayload buttonPayload = PollAddOptionButtonPayload
.builder()
.serverId(serverId)
.pollId(pollId)
.build();
ButtonConfigModel buttonConfigModel = ButtonConfigModel
.builder()
.buttonId(addOptionButtonId)
.buttonPayload(buttonPayload)
.origin(SERVER_POLL_ADD_OPTION_ORIGIN)
.payloadType(PollAddOptionButtonPayload.class)
.build();
componentPayloadManagementService.createButtonPayload(buttonConfigModel, serverId);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_TEMPLATE_KEY, model);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, PollPostTarget.POLLS, serverId);
return FutureUtils.toSingleFutureGeneric(messageFutures)
.thenAccept(unused -> self.persistPoll(messageFutures.get(0).join(), pollCreationRequest));
}
@Override
public CompletableFuture<Void> createQuickPoll(Member creator, List<String> options, String description,
Boolean allowMultiple, Boolean showDecisions, InteractionHook interactionHook, Duration pollDuration) {
HashSet<String> optionAsSet = new HashSet<>(options);
if(optionAsSet.size() != options.size()) {
throw new PollOptionAlreadyExistsException();
}
Long serverId = creator.getGuild().getIdLong();
Long pollId = counterService.getNextCounterValue(serverId, QUICK_POLLS_COUNTER_KEY);
log.info("Creating quick poll {} in server {} because of user {}.", pollId, serverId, creator.getIdLong());
List<PollMessageOption> parsedOptions = parseOptions(options);
String selectionMenuId = componentService.generateComponentId();
if(pollDuration == null) {
Long pollDurationSeconds = configService.getLongValueOrConfigDefault(PollService.QUICK_POLL_DURATION_SECONDS, serverId);
log.info("No duration provided - using {} seconds from configuration.", pollDurationSeconds);
pollDuration = Duration.ofSeconds(pollDurationSeconds);
}
Instant targetDate = Instant.now().plus(pollDuration);
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("serverId", serverId.toString());
parameters.put("pollId", pollId.toString());
JobParameters jobParameters = JobParameters.builder().parameters(parameters).build();
String triggerKey = schedulerService.executeJobWithParametersOnce("quickPollEvaluationJob", "poll", jobParameters, Date.from(targetDate));
log.info("Starting scheduled job to evaluate quick poll.");
PollCreationRequest pollCreationRequest = PollCreationRequest
.builder()
.pollId(pollId)
.type(PollType.QUICK)
.allowMultiple(allowMultiple)
.evaluationJobTrigger(triggerKey)
.showDecisions(showDecisions)
.selectionMenuId(selectionMenuId)
.serverId(serverId)
.allowAddition(false)
.targetDate(targetDate)
.creatorId(creator.getIdLong())
.description(description)
.options(parsedOptions)
.build();
QuickPollMessageModel model = QuickPollMessageModel
.builder()
.creator(MemberDisplay.fromMember(creator))
.description(description)
.pollId(pollId)
.allowMultiple(allowMultiple)
.showDecisions(showDecisions)
.endDate(targetDate)
.options(parsedOptions)
.selectionMenuId(selectionMenuId)
.build();
QuickPollSelectionMenuPayload payload = QuickPollSelectionMenuPayload
.builder()
.serverId(serverId)
.pollId(pollId)
.build();
SelectMenuConfigModel selectMenuConfigModel = SelectMenuConfigModel
.builder()
.selectMenuId(selectionMenuId)
.origin(QUICK_POLL_SELECTION_MENU_ORIGIN)
.selectMenuPayload(payload)
.payloadType(QuickPollSelectionMenuPayload.class)
.build();
componentPayloadManagementService.createStringSelectMenuPayload(selectMenuConfigModel, serverId);
MessageToSend messageToSend = templateService.renderEmbedTemplate(QUICK_POLL_TEMPLATE_KEY, model);
List<CompletableFuture<Message>> messageFutures = interactionService.sendMessageToInteraction(messageToSend, interactionHook);
return FutureUtils.toSingleFutureGeneric(messageFutures)
.thenAccept(unused -> self.persistPoll(messageFutures.get(0).join(), pollCreationRequest));
}
@Override
public CompletableFuture<Void> setDecisionsInPollTo(Member voter, List<String> chosenValues, Long pollId, PollType pollType) {
Poll poll = pollManagementService.getPollByPollId(pollId, voter.getGuild().getIdLong(), pollType);
log.info("Adding decisions of user {} to poll {}.", voter.getIdLong(), poll.getPollId());
AUserInAServer userInServer = userInServerManagementService.loadOrCreateUser(voter);
Optional<PollUserDecision> decisionOptional = pollUserDecisionManagementService.getUserDecisionOptional(poll, userInServer);
PollUserDecision decision;
boolean needToSave = false;
if(decisionOptional.isPresent()) {
decision = decisionOptional.get();
} else {
needToSave = true;
decision = pollUserDecisionManagementService.createUserDecision(poll, userInServer);
}
Long optionsAdded = 0L;
for (PollOption pollOption : poll.getOptions()) {
if (chosenValues.contains(pollOption.getValue()) &&
(decision.getOptions() == null || decision.getOptions().stream().noneMatch(pollUserDecisionOption -> pollUserDecisionOption.getPollOption().getLabel().equals(pollOption.getValue())))) {
pollUserDecisionOptionManagementService.addDecisionForUser(decision, pollOption);
optionsAdded += 1;
}
}
log.info("Added {} options to poll {} for user {}.", optionsAdded, pollId, voter.getIdLong());
if(decision.getOptions() != null) {
List<PollUserDecisionOption> toRemove = decision
.getOptions()
.stream()
.filter(pollUserDecisionOption -> !chosenValues.contains(pollUserDecisionOption.getPollOption().getLabel()))
.collect(Collectors.toList());
log.info("Removing {} options from poll {} for user {}.", toRemove.size(), pollId, voter.getIdLong());
pollUserDecisionOptionManagementService.deleteDecisionOptions(decision, toRemove);
}
if(needToSave) {
pollUserDecisionManagementService.savePollUserDecision(decision);
}
if(poll.getShowDecisions()) {
return updatePollMessage(poll, voter.getGuild());
} else {
return CompletableFuture.completedFuture(null);
}
}
@Override
public CompletableFuture<Void> addOptionToServerPoll(Long pollId, Long serverId, Member adder, String label, String description) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.STANDARD);
log.info("Adding option to server poll {} in server {}.", pollId, serverId);
pollOptionManagementService.addOptionToPoll(poll, label, description);
List<PollMessageOption> options = getOptionsOfPoll(poll);
ServerPollMessageModel model = ServerPollMessageModel.fromPoll(poll, options);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_TEMPLATE_KEY, model);
MessageChannel pollChannel = adder.getGuild().getChannelById(MessageChannel.class, poll.getChannel().getId());
List<CompletableFuture<Message>> messageFutures = channelService.editMessagesInAChannelFuture(messageToSend, pollChannel, Arrays.asList(poll.getMessageId()));
return FutureUtils.toSingleFutureGeneric(messageFutures);
}
@Override
@Transactional
public CompletableFuture<Void> evaluateServerPoll(Long pollId, Long serverId) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.STANDARD);
log.info("Evaluating server poll {} in server {}.", pollId, serverId);
poll.setState(PollState.FINISHED);
List<PollMessageOption> allOptions = getOptionsOfPoll(poll);
List<PollMessageOption> topOptions = allOptions;
if(!allOptions.isEmpty()) {
Integer mostVotes = allOptions
.stream()
.sorted(Comparator.comparingInt(PollMessageOption::getVotes).reversed())
.collect(Collectors.toList()).get(0).getVotes();
topOptions = allOptions
.stream()
.filter(pollMessageOption -> pollMessageOption.getVotes().equals(mostVotes))
.collect(Collectors.toList());
}
ServerPollEvaluationModel model = ServerPollEvaluationModel
.builder()
.pollId(pollId)
.options(allOptions)
.pollMessageId(poll.getMessageId())
.topOptions(topOptions)
.description(poll.getDescription())
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_EVALUATION_UPDATE_TEMPLATE_KEY, model);
log.info("Sending update message for poll evaluation of server poll {} in server {}.", pollId, serverId);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, PollPostTarget.POLLS, serverId);
GuildMessageChannel channel = channelService.getMessageChannelFromServer(serverId, poll.getChannel().getId());
log.info("Cleaning existing components in message {} for server poll {} in server {}.", poll.getMessageId(), pollId, serverId);
CompletableFuture<Message> cleanMessageFuture = channelService.removeComponents(channel, poll.getMessageId());
return CompletableFuture.allOf(FutureUtils.toSingleFutureGeneric(messageFutures), cleanMessageFuture)
.thenAccept(unused -> self.updateFinalPollMessage(pollId, channel.getGuild()));
}
@Override
@Transactional
public CompletableFuture<Void> remindServerPoll(Long pollId, Long serverId) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.STANDARD);
log.info("Reminding about server poll {} in server {}.", pollId, serverId);
List<PollMessageOption> allOptions = getOptionsOfPoll(poll);
List<PollMessageOption> topOptions = allOptions;
if(!allOptions.isEmpty()) {
Integer mostVotes = allOptions
.stream()
.sorted(Comparator.comparingInt(PollMessageOption::getVotes).reversed())
.collect(Collectors.toList()).get(0).getVotes();
topOptions = allOptions
.stream()
.filter(pollMessageOption -> pollMessageOption.getVotes().equals(mostVotes))
.collect(Collectors.toList());
}
ServerPollReminderModel model = ServerPollReminderModel
.builder()
.pollId(pollId)
.options(allOptions)
.topOptions(topOptions)
.messageLink(MessageUtils.buildMessageUrl(serverId, poll.getChannel().getId(), poll.getMessageId()))
.description(poll.getDescription())
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_REMINDER_TEMPLATE_KEY, model);
log.info("Sending poll reminder about server poll {} in server {}.", pollId, serverId);
return FutureUtils.toSingleFutureGeneric(postTargetService.sendEmbedInPostTarget(messageToSend, PollPostTarget.POLL_REMINDER, serverId));
}
@Override
@Transactional
public CompletableFuture<Void> evaluateQuickPoll(Long pollId, Long serverId) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.QUICK);
log.info("Evaluating quick poll {} in server {}.", pollId, serverId);
poll.setState(PollState.FINISHED);
List<PollMessageOption> allOptions = getOptionsOfPoll(poll);
List<PollMessageOption> topOptions = allOptions;
if(!allOptions.isEmpty()) {
Integer mostVotes = allOptions
.stream()
.sorted(Comparator.comparingInt(PollMessageOption::getVotes).reversed())
.collect(Collectors.toList()).get(0).getVotes();
topOptions = allOptions
.stream()
.filter(pollMessageOption -> pollMessageOption.getVotes().equals(mostVotes))
.collect(Collectors.toList());
}
QuickPollEvaluationModel model = QuickPollEvaluationModel
.builder()
.pollId(pollId)
.options(allOptions)
.pollMessageId(poll.getMessageId())
.topOptions(topOptions)
.description(poll.getDescription())
.build();
MessageChannel channel = channelService.getMessageChannelFromServer(serverId, poll.getChannel().getId());
CompletableFuture<Message> removeComponentFuture = channelService.removeComponents(channel, poll.getMessageId());
MessageToSend messageToSend = templateService.renderEmbedTemplate(QUICK_POLL_EVALUATION_UPDATE_TEMPLATE_KEY, model);
CompletableFuture<Void> updateMessageFuture = FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, channel));
return CompletableFuture.allOf(removeComponentFuture, updateMessageFuture)
.thenApply(message -> null);
}
@Override
public CompletableFuture<Void> closePoll(Long pollId, Long serverId, String text, Member cause) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.STANDARD);
log.info("Member {} closes poll {} in server {}.", cause.getIdLong(), pollId, serverId);
PollClosingMessageModel model = PollClosingMessageModel
.builder()
.pollMessageId(poll.getMessageId())
.cause(MemberNameDisplay.fromMember(cause))
.pollId(pollId)
.text(text)
.serverId(serverId)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_CLOSE_MESSAGE, model);
List<CompletableFuture<Message>> messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, PollPostTarget.POLLS, serverId);
MessageChannel channel = channelService.getMessageChannelFromServer(serverId, poll.getChannel().getId());
CompletableFuture<Message> removeComponentsFuture = channelService.removeComponents(channel, poll.getMessageId());
return CompletableFuture.allOf(FutureUtils.toSingleFutureGeneric(messageFutures), removeComponentsFuture);
}
@Override
public CompletableFuture<Void> cancelPoll(Long pollId, Long serverId, Member cause) {
Poll poll = pollManagementService.getPollByPollId(pollId, serverId, PollType.STANDARD);
log.info("Member {} cancelled poll {} in server {}.", cause.getIdLong(), pollId, serverId);
if(!poll.getCreator().getUserReference().getId().equals(cause.getIdLong()) ||
poll.getCreated().isBefore(Instant.now().minus(Duration.ofSeconds(removalMaxAgeSeconds)))) {
throw new PollCancellationNotPossibleException();
}
if(poll.getReminderJobTriggerKey() != null) {
schedulerService.stopTrigger(poll.getReminderJobTriggerKey());
}
if(poll.getEvaluationJobTriggerKey() != null) {
schedulerService.stopTrigger(poll.getEvaluationJobTriggerKey());
}
poll.setState(PollState.CANCELLED);
return messageService.deleteMessageInChannelInServer(serverId, poll.getChannel().getId(), poll.getMessageId());
}
@Transactional
public CompletableFuture<Void> updateFinalPollMessage(Long pollId, Guild guild) {
Poll poll = pollManagementService.getPollByPollId(pollId, guild.getIdLong(), PollType.STANDARD);
List<PollMessageOption> options = getOptionsOfPoll(poll);
ServerPollMessageModel model = ServerPollMessageModel.fromPoll(poll, options);
model.setAllowAdditions(false);
model.setShowDecisions(true);
model.setAllowMultiple(false);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_TEMPLATE_KEY, model);
MessageChannel pollChannel = guild.getChannelById(MessageChannel.class, poll.getChannel().getId());
return channelService.editEmbedMessageInAChannel(messageToSend.getEmbeds().get(0), pollChannel, poll.getMessageId())
.thenApply(message -> null);
}
public CompletableFuture<Void> updatePollMessage(Poll poll, Guild guild) {
List<PollMessageOption> options = getOptionsOfPoll(poll);
ServerPollMessageModel model = ServerPollMessageModel.fromPoll(poll, options);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SERVER_POLL_TEMPLATE_KEY, model);
MessageChannel pollChannel = guild.getChannelById(MessageChannel.class, poll.getChannel().getId());
return channelService.editEmbedMessageInAChannel(messageToSend.getEmbeds().get(0), pollChannel, poll.getMessageId())
.thenApply(message -> null);
}
@Transactional
public void persistPoll(Message message, PollCreationRequest pollCreationRequest) {
if(message == null) {
log.info("Post target was not setup - no message created.");
return;
}
pollCreationRequest.setPollMessageId(message.getIdLong());
pollCreationRequest.setPollChannelId(message.getChannel().getIdLong());
log.info("Persisting poll {} shown in message {} in channel {} in server {}.",
pollCreationRequest.getPollId(), pollCreationRequest.getPollMessageId(), pollCreationRequest.getPollChannelId(),
pollCreationRequest.getServerId());
Poll createdPoll = pollManagementService.createPoll(pollCreationRequest);
log.info("Adding {} options to poll {}.", pollCreationRequest.getOptions().size(), pollCreationRequest.getPollId());
pollOptionManagementService.addOptionsToPoll(createdPoll, pollCreationRequest);
}
private List<PollMessageOption> parseOptions(List<String> options) {
return options.stream().map(s -> {
String label = s;
String description = "";
if(s.contains(";")) {
String[] splitOption = s.split(";");
label = splitOption[0];
description = splitOption[1];
}
return PollMessageOption
.builder()
.label(label)
.value(label)
.votes(0)
.percentage(0f)
.description(description)
.build();
}).collect(Collectors.toList());
}
private List<PollMessageOption> getOptionsOfPoll(Poll poll) {
Integer totalVotes = poll
.getDecisions()
.stream()
.map(userDecision -> userDecision.getOptions().size())
.mapToInt(Integer::intValue)
.sum();
return poll.getOptions().stream().map(option -> {
Long voteCount = poll
.getDecisions()
.stream()
.filter(decision -> decision.getOptions().stream().anyMatch(pollUserDecisionOption -> pollUserDecisionOption.getPollOption().equals(option)))
.count();
return PollMessageOption
.builder()
.value(option.getValue())
.label(option.getLabel())
.votes(voteCount.intValue())
.percentage(totalVotes > 0 ? (voteCount / (float) totalVotes) * 100 : 0)
.description(option.getDescription())
.build();
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,75 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.suggestion.exception.PollNotFoundException;
import dev.sheldan.abstracto.suggestion.model.PollCreationRequest;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollState;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import dev.sheldan.abstracto.suggestion.repository.PollRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
@Slf4j
public class PollManagementServiceBean implements PollManagementService {
@Autowired
private PollRepository pollRepository;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private ChannelManagementService channelManagementService;
@Override
public Poll createPoll(PollCreationRequest pollCreationRequest) {
ServerUser creatorServerUser = ServerUser
.builder()
.userId(pollCreationRequest.getCreatorId())
.serverId(pollCreationRequest.getServerId())
.build();
AUserInAServer creator = userInServerManagementService.loadOrCreateUser(creatorServerUser);
AChannel channel = channelManagementService.loadChannel(pollCreationRequest.getPollChannelId());
Poll pollInstance = Poll
.builder()
.description(pollCreationRequest.getDescription())
.server(creator.getServerReference())
.pollId(pollCreationRequest.getPollId())
.allowMultiple(pollCreationRequest.getAllowMultiple())
.allowAddition(pollCreationRequest.getAllowAddition())
.showDecisions(pollCreationRequest.getShowDecisions())
.reminderJobTriggerKey(pollCreationRequest.getReminderJobTrigger())
.targetDate(pollCreationRequest.getTargetDate())
.evaluationJobTriggerKey(pollCreationRequest.getEvaluationJobTrigger())
.messageId(pollCreationRequest.getPollMessageId())
.channel(channel)
.addOptionButtonId(pollCreationRequest.getAddOptionButtonId())
.selectionMenuId(pollCreationRequest.getSelectionMenuId())
.creator(creator)
.state(PollState.NEW)
.type(pollCreationRequest.getType())
.build();
return pollRepository.save(pollInstance);
}
@Override
public Poll getPollByPollId(Long pollId, Long serverId, PollType pollType) {
return getPollByPollIdOptional(pollId, serverId, pollType).orElseThrow(() -> new PollNotFoundException(pollId));
}
@Override
public Optional<Poll> getPollByPollIdOptional(Long pollId, Long serverId, PollType pollType) {
return pollRepository.findByPollIdAndServer_IdAndType(pollId, serverId, pollType);
}
}

View File

@@ -0,0 +1,52 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.suggestion.model.PollCreationRequest;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollOption;
import dev.sheldan.abstracto.suggestion.repository.PollOptionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
public class PollOptionManagementServiceBean implements PollOptionManagementService {
@Autowired
private PollOptionRepository pollOptionRepository;
@Override
public void addOptionsToPoll(Poll poll, PollCreationRequest pollCreationRequest) {
List<PollOption> options = pollCreationRequest.getOptions().stream().map(option -> PollOption
.builder()
.poll(poll)
.server(poll.getServer())
.label(option.getLabel())
.value(option.getLabel())
.description(option.getDescription())
.build()).collect(Collectors.toList());
pollOptionRepository.saveAll(options);
}
@Override
public void addOptionToPoll(Poll poll, String label, String description) {
PollOption option = PollOption
.builder()
.poll(poll)
.label(label)
.value(label)
.server(poll.getServer())
.description(description)
.build();
pollOptionRepository.save(option);
}
@Override
public Optional<PollOption> getPollOptionByName(Poll poll, String key) {
return Optional.empty();
}
}

View File

@@ -0,0 +1,51 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecision;
import dev.sheldan.abstracto.suggestion.repository.PollUserDecisionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Optional;
@Component
public class PollUserDecisionManagementServiceBean implements PollUserDecisionManagementService {
@Autowired
private PollUserDecisionRepository repository;
@Override
public PollUserDecision addUserDecision(Poll poll, AUserInAServer user) {
return repository.save(createUserDecision(poll, user));
}
@Override
public PollUserDecision createUserDecision(Poll poll, AUserInAServer user) {
return PollUserDecision
.builder()
.server(user.getServerReference())
.voter(user)
.options(new ArrayList<>())
.poll(poll)
.build();
}
@Override
public Optional<PollUserDecision> getUserDecisionOptional(Poll poll, AUserInAServer user) {
return repository.findPollUserDecisionByPollAndVoter(poll, user);
}
@Override
public PollUserDecision getUserDecision(Poll poll, AUserInAServer user) {
return repository.findPollUserDecisionByPollAndVoter(poll, user).orElseThrow(() -> new AbstractoRunTimeException("User decision not found."));
}
@Override
public void savePollUserDecision(PollUserDecision pollUserDecision) {
repository.save(pollUserDecision);
}
}

View File

@@ -0,0 +1,41 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.suggestion.model.database.PollOption;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecision;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecisionOption;
import dev.sheldan.abstracto.suggestion.repository.PollUserDecisionOptionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class PollUserDecisionOptionManagementServiceBean implements PollUserDecisionOptionManagementService {
@Autowired
private PollUserDecisionOptionRepository repository;
@Override
public PollUserDecisionOption addDecisionForUser(PollUserDecision decision, PollOption pollOption) {
PollUserDecisionOption option = PollUserDecisionOption
.builder()
.decision(decision)
.poll(decision.getPoll())
.pollOption(pollOption)
.build();
decision.getOptions().add(option);
return option;
}
@Override
public void clearOptions(PollUserDecision pollUserDecision) {
repository.deleteAll(pollUserDecision.getOptions());
}
@Override
public void deleteDecisionOptions(PollUserDecision decision, List<PollUserDecisionOption> decisionOptionList) {
decision.getOptions().removeAll(decisionOptionList);
repository.deleteAll(decisionOptionList);
}
}

View File

@@ -0,0 +1,11 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,35 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<property name="utilityModule" value="(SELECT id FROM module WHERE name = 'utility')"/>
<property name="pollFeature" value="(SELECT id FROM feature WHERE key = 'poll')"/>
<changeSet author="Sheldan" id="poll-commands">
<insert tableName="command">
<column name="name" value="poll"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${pollFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="quickPoll"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${pollFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="closePoll"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${pollFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="cancelPoll"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${pollFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,12 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="feature.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
<include file="poll_jobs.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll_feature-insertion">
<insert tableName="feature">
<column name="key" value="poll"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,32 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll_jobs-insert">
<insert tableName="scheduler_job">
<column name="name" value="serverPollEvaluationJob"/>
<column name="group_name" value="poll"/>
<column name="clazz" value="dev.sheldan.abstracto.suggestion.job.ServerPollEvaluationJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
<insert tableName="scheduler_job">
<column name="name" value="quickPollEvaluationJob"/>
<column name="group_name" value="poll"/>
<column name="clazz" value="dev.sheldan.abstracto.suggestion.job.QuickPollEvaluationJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
<insert tableName="scheduler_job">
<column name="name" value="serverPollReminderJob"/>
<column name="group_name" value="poll"/>
<column name="clazz" value="dev.sheldan.abstracto.suggestion.job.ServerPollReminderJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,89 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll-table">
<createTable tableName="poll">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="poll_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="type" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="state" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="selection_menu_id" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="add_option_button_id" type="VARCHAR(100)">
<constraints nullable="true"/>
</column>
<column name="creator_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(2000)">
<constraints nullable="false"/>
</column>
<column name="evaluation_job_trigger_key" type="varchar(255)"/>
<column name="reminder_job_trigger_key" type="varchar(255)"/>
<column name="target_date" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="allow_multiple" type="BOOLEAN">
<constraints nullable="false"/>
</column>
<column name="show_decisions" type="BOOLEAN">
<constraints nullable="false"/>
</column>
<column name="allow_addition" type="BOOLEAN">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint
columnNames="poll_id, server_id, type"
constraintName="uq_poll_id"
tableName="poll"
/>
<addForeignKeyConstraint baseColumnNames="channel_id" baseTableName="poll" constraintName="fk_poll_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="creator_user_in_server_id" baseTableName="poll" constraintName="fk_poll_creator"
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="poll" constraintName="fk_poll_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS poll_update_trigger ON poll;
CREATE TRIGGER poll_update_trigger BEFORE UPDATE ON poll FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS poll_insert_trigger ON poll;
CREATE TRIGGER poll_insert_trigger BEFORE INSERT ON poll FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
<sql>
ALTER TABLE poll ADD CONSTRAINT check_poll_state CHECK (state IN ('NEW', 'FINISHED','CANCELLED', 'VETOED'));
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,56 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll_option-table">
<createTable tableName="poll_option">
<column name="id" autoIncrement="true" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="poll_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="label" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="value" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="adder_user_in_server_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</createTable>
<addPrimaryKey columnNames="id" tableName="poll_option" constraintName="pk_poll_option" validate="true"/>
<addForeignKeyConstraint baseColumnNames="adder_user_in_server_id" baseTableName="poll_option" constraintName="fk_poll_option_adder"
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="poll_option" constraintName="fk_poll_option_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="poll_id" baseTableName="poll_option" constraintName="fk_poll_option_poll"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="poll" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS poll_option_update_trigger ON poll_option;
CREATE TRIGGER poll_option_update_trigger BEFORE UPDATE ON poll_option FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS poll_option_insert_trigger ON poll_option;
CREATE TRIGGER poll_option_insert_trigger BEFORE INSERT ON poll_option FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,51 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll_user_decision-table">
<createTable tableName="poll_user_decision">
<column name="id" autoIncrement="true" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="poll_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint
columnNames="user_in_server_id, poll_id"
constraintName="uq_poll_user_decision"
tableName="poll_user_decision"
/>
<addForeignKeyConstraint baseColumnNames="user_in_server_id" baseTableName="poll_user_decision" constraintName="fk_poll_user_decision_user"
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="poll_user_decision" constraintName="fk_poll_user_decision_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="poll_id" baseTableName="poll_user_decision" constraintName="fk_poll_user_decision_poll"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="poll" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS poll_user_decision_update_trigger ON poll_user_decision;
CREATE TRIGGER poll_user_decision_update_trigger BEFORE UPDATE ON poll_user_decision FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS poll_user_decision_insert_trigger ON poll_user_decision;
CREATE TRIGGER poll_user_decision_insert_trigger BEFORE INSERT ON poll_user_decision FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,51 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="poll_user_decision_option-table">
<createTable tableName="poll_user_decision_option">
<column name="id" autoIncrement="true" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="user_decision_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="poll_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="option_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
</createTable>
<addUniqueConstraint
columnNames="user_decision_id, poll_id, option_id"
constraintName="uq_poll_user_decision_option"
tableName="poll_user_decision_option"
/>
<addForeignKeyConstraint baseColumnNames="user_decision_id" baseTableName="poll_user_decision_option" constraintName="fk_poll_user_decision_option_decision"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="poll_user_decision" validate="true"/>
<addForeignKeyConstraint baseColumnNames="option_id" baseTableName="poll_user_decision_option" constraintName="fk_poll_user_decision_option_option"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="poll_option" validate="true"/>
<addForeignKeyConstraint baseColumnNames="poll_id" baseTableName="poll_user_decision_option" constraintName="fk_poll_user_decision_option_poll"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="poll" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS poll_user_decision_option_update_trigger ON poll_user_decision_option;
CREATE TRIGGER poll_user_decision_option_trigger BEFORE UPDATE ON poll_user_decision_option FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS poll_user_decision_option_insert_trigger ON poll_user_decision_option;
CREATE TRIGGER poll_user_decision_option_insert_trigger BEFORE INSERT ON poll_user_decision_option FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,13 @@
<?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.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="poll.xml" relativeToChangelogFile="true"/>
<include file="poll_option.xml" relativeToChangelogFile="true"/>
<include file="poll_user_decision.xml" relativeToChangelogFile="true"/>
<include file="poll_user_decision_option.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -12,4 +12,5 @@
<include file="1.3.8/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.0/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.8/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.26/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -30,4 +30,26 @@ abstracto.featureModes.suggestionAutoEvaluate.enabled=false
abstracto.featureModes.suggestionButton.featureName=suggestion
abstracto.featureModes.suggestionButton.mode=suggestionButton
abstracto.featureModes.suggestionButton.enabled=true
abstracto.featureModes.suggestionButton.enabled=true
abstracto.featureFlags.poll.featureName=poll
abstracto.featureFlags.poll.enabled=false
abstracto.postTargets.poll.name=polls
abstracto.postTargets.pollReminder.name=pollReminder
abstracto.featureModes.pollAutoEvaluate.featureName=poll
abstracto.featureModes.pollAutoEvaluate.mode=pollAutoEvaluate
abstracto.featureModes.pollAutoEvaluate.enabled=false
abstracto.featureModes.pollReminder.featureName=poll
abstracto.featureModes.pollReminder.mode=pollReminder
abstracto.featureModes.pollReminder.enabled=false
abstracto.systemConfigs.serverPollDurationSeconds.name=serverPollDurationSeconds
abstracto.systemConfigs.serverPollDurationSeconds.longValue=604800
abstracto.systemConfigs.quickPollDurationSeconds.name=quickPollDurationSeconds
abstracto.systemConfigs.quickPollDurationSeconds.longValue=90
abstracto.feature.poll.removalMaxAge=3600

View File

@@ -0,0 +1,34 @@
package dev.sheldan.abstracto.suggestion.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureMode;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import dev.sheldan.abstracto.suggestion.service.PollService;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class PollFeatureConfig implements FeatureConfig {
@Override
public FeatureDefinition getFeature() {
return SuggestionFeatureDefinition.POLL;
}
@Override
public List<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(PollPostTarget.POLLS, PollPostTarget.POLL_REMINDER);
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(PollService.SERVER_POLL_DURATION_SECONDS);
}
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(PollFeatureMode.POLL_AUTO_EVALUATE, PollFeatureMode.POLL_REMINDER);
}
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.suggestion.config;
import dev.sheldan.abstracto.core.config.FeatureMode;
import lombok.Getter;
@Getter
public enum PollFeatureMode implements FeatureMode {
POLL_AUTO_EVALUATE("pollAutoEvaluate"),
POLL_REMINDER("pollReminder");
private final String key;
PollFeatureMode(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.suggestion.config;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import lombok.Getter;
@Getter
public enum PollPostTarget implements PostTargetEnum {
POLLS("polls"), POLL_REMINDER("pollReminder");
private String key;
PollPostTarget(String key) {
this.key = key;
}
}

View File

@@ -37,13 +37,16 @@ public class SuggestionFeatureConfig implements FeatureConfig {
SuggestionFeatureMode.SUGGESTION_REMINDER,
SuggestionFeatureMode.SUGGESTION_BUTTONS,
SuggestionFeatureMode.SUGGESTION_AUTO_EVALUATE,
SuggestionFeatureMode.SUGGESTION_THREAD);
SuggestionFeatureMode.SUGGESTION_THREAD
);
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(SuggestionService.SUGGESTION_REMINDER_DAYS_CONFIG_KEY,
return Arrays.asList(
SuggestionService.SUGGESTION_REMINDER_DAYS_CONFIG_KEY,
SuggestionService.SUGGESTION_AUTO_EVALUATE_DAYS_CONFIG_KEY,
SuggestionService.SUGGESTION_AUTO_EVALUATE_PERCENTAGE_CONFIG_KEY);
SuggestionService.SUGGESTION_AUTO_EVALUATE_PERCENTAGE_CONFIG_KEY
);
}
}

View File

@@ -5,7 +5,7 @@ import lombok.Getter;
@Getter
public enum SuggestionFeatureDefinition implements FeatureDefinition {
SUGGEST("suggestion");
SUGGEST("suggestion"), POLL("poll");
private String key;

View File

@@ -1,6 +1,11 @@
package dev.sheldan.abstracto.suggestion.config;
public class SuggestionSlashCommandNames {
private SuggestionSlashCommandNames() {
}
public static final String SUGGEST = "suggest";
public static final String SUGGEST_PUBLIC = "suggestpublic";
public static final String POLL_PUBLIC = "pollpublic";
public static final String POLL = "poll";
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.suggestion.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
public class PollCancellationNotPossibleException extends AbstractoRunTimeException implements Templatable {
public PollCancellationNotPossibleException() {
super("Not possible to cancel poll.");
}
@Override
public String getTemplateName() {
return "poll_cancellation_not_possible_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,27 @@
package dev.sheldan.abstracto.suggestion.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
import dev.sheldan.abstracto.suggestion.model.exception.PollNotFoundExceptionModel;
public class PollNotFoundException extends AbstractoRunTimeException implements Templatable {
private final PollNotFoundExceptionModel model;
public PollNotFoundException(Long pollId) {
super("Poll not found");
this.model = PollNotFoundExceptionModel
.builder()
.pollId(pollId)
.build();
}
@Override
public String getTemplateName() {
return "poll_does_not_exist_exception";
}
@Override
public Object getTemplateModel() {
return model;
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.suggestion.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
public class PollOptionAlreadyExistsException extends AbstractoRunTimeException implements Templatable {
public PollOptionAlreadyExistsException() {
super("Poll option already exists.");
}
@Override
public String getTemplateName() {
return "poll_option_already_exists_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,33 @@
package dev.sheldan.abstracto.suggestion.model;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import dev.sheldan.abstracto.suggestion.model.template.PollMessageOption;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Builder
@Getter
public class PollCreationRequest {
private Long pollId;
private String description;
private List<PollMessageOption> options;
private Boolean allowMultiple;
private Boolean allowAddition;
private Boolean showDecisions;
private String evaluationJobTrigger;
private String reminderJobTrigger;
private String addOptionButtonId;
private Instant targetDate;
private String selectionMenuId;
private Long serverId;
@Setter
private Long pollChannelId;
private Long creatorId;
@Setter
private Long pollMessageId;
private PollType type;
}

View File

@@ -0,0 +1,101 @@
package dev.sheldan.abstracto.suggestion.model.database;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name="poll")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class Poll {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "poll_id", nullable = false)
private Long pollId;
@Column(name = "message_id", nullable = false)
private Long messageId;
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false)
private PollType type;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_user_in_server_id", nullable = false)
private AUserInAServer creator;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "channel_id", nullable = false)
private AChannel channel;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Enumerated(EnumType.STRING)
@Column(name = "state", nullable = false)
private PollState state;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
@Column(name = "description", nullable = false)
private String description;
@Column(name = "evaluation_job_trigger_key")
private String evaluationJobTriggerKey;
@Column(name = "reminder_job_trigger_key")
private String reminderJobTriggerKey;
@Column(name = "target_date")
private Instant targetDate;
@Column(name = "allow_multiple")
private Boolean allowMultiple;
@Column(name = "show_decisions")
private Boolean showDecisions;
@Column(name = "allow_addition")
private Boolean allowAddition;
@Column(name = "selection_menu_id")
private String selectionMenuId;
@Column(name = "add_option_button_id")
private String addOptionButtonId;
@OneToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "poll")
@Builder.Default
private List<PollOption> options = new ArrayList<>();
@OneToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "poll")
@Builder.Default
private List<PollUserDecision> decisions = new ArrayList<>();
}

View File

@@ -0,0 +1,53 @@
package dev.sheldan.abstracto.suggestion.model.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name="poll_option")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class PollOption {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "poll_id", nullable = false)
private Poll poll;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Column(name = "label", nullable = false)
private String label;
@Column(name = "value", nullable = false)
private String value;
@Column(name = "description", nullable = false)
private String description;
@Getter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "adder_user_in_server_id")
private AUserInAServer adder;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.abstracto.suggestion.model.database;
public enum PollState {
NEW, FINISHED, CANCELLED, VETOED
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.abstracto.suggestion.model.database;
public enum PollType {
STANDARD, QUICK
}

View File

@@ -0,0 +1,51 @@
package dev.sheldan.abstracto.suggestion.model.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name="poll_user_decision")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class PollUserDecision {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
@JoinColumn(name = "user_in_server_id", nullable = false)
private AUserInAServer voter;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "poll_id", nullable = false)
private Poll poll;
@OneToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "decision", orphanRemoval = true)
@Builder.Default
private List<PollUserDecisionOption> options = new ArrayList<>();
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -0,0 +1,40 @@
package dev.sheldan.abstracto.suggestion.model.database;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name="poll_user_decision_option")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class PollUserDecisionOption {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_decision_id", nullable = false)
private PollUserDecision decision;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "option_id", nullable = false)
private PollOption pollOption;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "poll_id", nullable = false)
private Poll poll;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -9,6 +9,8 @@ import lombok.*;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name="suggestion")
@@ -58,6 +60,13 @@ public class Suggestion implements Serializable {
@Column(name = "suggestion_text", nullable = false)
private String suggestionText;
@OneToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "suggestion")
@Builder.Default
private List<SuggestionVote> votes = new ArrayList<>();
@Getter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "command_channel_id", nullable = false)

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.suggestion.model.exception;
import lombok.Builder;
import lombok.Getter;
import java.io.Serializable;
@Getter
@Builder
public class PollNotFoundExceptionModel implements Serializable {
private final Long pollId;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.suggestion.model.payload;
import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class PollAddOptionButtonPayload implements ButtonPayload {
private Long pollId;
private Long serverId;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.suggestion.model.payload;
import dev.sheldan.abstracto.core.interaction.modal.ModalPayload;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class PollAddOptionModalPayload implements ModalPayload {
private String modalId;
private String labelInputComponentId;
private String descriptionInputComponentId;
private Long serverId;
private Long pollId;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.suggestion.model.payload;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuPayload;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class QuickPollSelectionMenuPayload implements SelectMenuPayload {
private Long pollId;
private Long serverId;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.suggestion.model.payload;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuPayload;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ServerPollSelectionMenuPayload implements SelectMenuPayload {
private Long pollId;
private Long serverId;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.suggestion.model.template;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class PollAddOptionModalModel {
private String modalId;
private String labelInputComponentId;
private String descriptionInputComponentId;
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.suggestion.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class PollAddOptionNotificationModel {
private String label;
private String description;
private String value;
private Long pollId;
private Long serverId;
private MemberNameDisplay memberNameDisplay;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.suggestion.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class PollClosingMessageModel {
private Long pollId;
private Long serverId;
private MemberNameDisplay cause;
private String text;
private Long pollMessageId;
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.suggestion.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberNameDisplay;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class PollDecisionNotificationModel {
private List<String> chosenValues;
private Long pollId;
private Long serverId;
private MemberNameDisplay memberNameDisplay;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.suggestion.model.template;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class PollMessageOption {
private String value;
private String label;
private String description;
private Integer votes;
private Float percentage;
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.suggestion.model.template;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class QuickPollEvaluationModel {
private Long pollId;
private String description;
private Long pollMessageId;
private List<PollMessageOption> topOptions;
private List<PollMessageOption> options;
}

View File

@@ -0,0 +1,24 @@
package dev.sheldan.abstracto.suggestion.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@Builder
public class QuickPollMessageModel {
private MemberDisplay creator;
private Long pollId;
private String description;
private String selectionMenuId;
private String addOptionButtonId;
private Boolean allowMultiple;
private Instant endDate;
private Boolean showDecisions;
private List<PollMessageOption> options;
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.suggestion.model.template;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class ServerPollEvaluationModel {
private Long pollId;
private String description;
private Long pollMessageId;
private List<PollMessageOption> topOptions;
private List<PollMessageOption> options;
}

View File

@@ -0,0 +1,45 @@
package dev.sheldan.abstracto.suggestion.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollState;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
@Builder
public class ServerPollMessageModel {
private MemberDisplay creator;
private Long pollId;
private PollState state;
private String description;
private String selectionMenuId;
private String addOptionButtonId;
private Boolean allowMultiple;
private Boolean showDecisions;
private Boolean allowAdditions;
private Instant endDate;
private List<PollMessageOption> options;
public static ServerPollMessageModel fromPoll(Poll poll, List<PollMessageOption> options) {
return ServerPollMessageModel
.builder()
.creator(MemberDisplay.fromAUserInAServer(poll.getCreator()))
.description(poll.getDescription())
.pollId(poll.getId())
.state(poll.getState())
.allowMultiple(poll.getAllowMultiple())
.showDecisions(poll.getShowDecisions())
.endDate(poll.getTargetDate())
.allowAdditions(poll.getAllowAddition())
.options(options)
.addOptionButtonId(poll.getAddOptionButtonId())
.selectionMenuId(poll.getSelectionMenuId())
.build();
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.suggestion.model.template;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class ServerPollReminderModel {
private Long pollId;
private String description;
private String messageLink;
private Long pollMessageId;
private List<PollMessageOption> topOptions;
private List<PollMessageOption> options;
}

View File

@@ -0,0 +1,29 @@
package dev.sheldan.abstracto.suggestion.service;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.interactions.InteractionHook;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface PollService {
String SERVER_POLL_DURATION_SECONDS = "serverPollDurationSeconds";
String QUICK_POLL_DURATION_SECONDS = "quickPollDurationSeconds";
CompletableFuture<Void> createServerPoll(Member creator, List<String> options, String description,
Boolean allowMultiple, Boolean allowAddition, Boolean showDecisions, Duration duration);
CompletableFuture<Void> createQuickPoll(Member creator, List<String> options, String description,
Boolean allowMultiple, Boolean showDecisions, InteractionHook interactionHook, Duration duration);
CompletableFuture<Void> setDecisionsInPollTo(Member voter, List<String> chosenValues, Long pollId, PollType pollType);
CompletableFuture<Void> addOptionToServerPoll(Long pollId, Long serverId, Member adder, String label, String description);
CompletableFuture<Void> evaluateServerPoll(Long pollId, Long serverId);
CompletableFuture<Void> remindServerPoll(Long pollId, Long serverId);
CompletableFuture<Void> evaluateQuickPoll(Long pollId, Long serverId);
CompletableFuture<Void> closePoll(Long pollId, Long serverId, String text, Member cause);
CompletableFuture<Void> cancelPoll(Long pollId, Long serverId, Member cause);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.suggestion.model.PollCreationRequest;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollType;
import java.util.Optional;
public interface PollManagementService {
Poll createPoll(PollCreationRequest pollCreationRequest);
Poll getPollByPollId(Long pollId, Long serverId, PollType pollType);
Optional<Poll> getPollByPollIdOptional(Long pollId, Long serverId, PollType pollType);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.suggestion.model.PollCreationRequest;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollOption;
import java.util.Optional;
public interface PollOptionManagementService {
void addOptionsToPoll(Poll poll, PollCreationRequest pollCreationRequest);
void addOptionToPoll(Poll poll, String label, String description);
Optional<PollOption> getPollOptionByName(Poll poll, String key);
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.suggestion.model.database.Poll;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecision;
import java.util.Optional;
public interface PollUserDecisionManagementService {
PollUserDecision addUserDecision(Poll poll, AUserInAServer user);
PollUserDecision createUserDecision(Poll poll, AUserInAServer user);
Optional<PollUserDecision> getUserDecisionOptional(Poll poll, AUserInAServer user);
PollUserDecision getUserDecision(Poll poll, AUserInAServer user);
void savePollUserDecision(PollUserDecision pollUserDecision);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.suggestion.service.management;
import dev.sheldan.abstracto.suggestion.model.database.PollOption;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecision;
import dev.sheldan.abstracto.suggestion.model.database.PollUserDecisionOption;
import java.util.List;
public interface PollUserDecisionOptionManagementService {
PollUserDecisionOption addDecisionForUser(PollUserDecision decision, PollOption pollOption);
void clearOptions(PollUserDecision pollUserDecision);
void deleteDecisionOptions(PollUserDecision decision, List<PollUserDecisionOption> decisionOptionList);
}

View File

@@ -72,6 +72,11 @@ public class ListenerExecutorConfig {
return executorService.setupExecutorFor("buttonClickedListener");
}
@Bean(name = "stringSelectMenuExecutor")
public TaskExecutor stringSelectMenuExecutor() {
return executorService.setupExecutorFor("stringSelectMenuListener");
}
@Bean(name = "modalInteractionExecutor")
public TaskExecutor modalInteractionExecutor() {
return executorService.setupExecutorFor("modalInteractionListener");

View File

@@ -101,7 +101,7 @@ public class InteractionServiceBean implements InteractionService {
if(component instanceof ActionComponent) {
String id = ((ActionComponent)component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload.getPersistCallback()) {
if(payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
}
@@ -217,7 +217,7 @@ public class InteractionServiceBean implements InteractionService {
if(component instanceof ActionComponent) {
String id = ((ActionComponent)component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload.getPersistCallback()) {
if(payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
}
@@ -273,7 +273,7 @@ public class InteractionServiceBean implements InteractionService {
if(component instanceof ActionComponent) {
String id = ((ActionComponent)component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload.getPersistCallback()) {
if(payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
}
@@ -303,6 +303,11 @@ public class InteractionServiceBean implements InteractionService {
return replyMessageToSend(messageToSend, callback);
}
@Override
public CompletableFuture<Message> replyString(String text, InteractionHook interactionHook) {
return interactionHook.sendMessage(text).submit();
}
@PostConstruct
public void postConstruct() {
metricService.registerCounter(EPHEMERAL_MESSAGES_SEND, "Ephemeral messages send");

View File

@@ -62,8 +62,6 @@ public class SyncButtonClickedListenerBean extends ListenerAdapter {
@Override
public void onButtonInteraction(@Nonnull ButtonInteractionEvent event) {
if(listenerList == null) return;
// TODO remove this and make this configurable
event.deferEdit().queue();
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), buttonClickedExecutor).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async button event.", throwable);
return null;
@@ -83,6 +81,9 @@ public class SyncButtonClickedListenerBean extends ListenerAdapter {
if(listenerOptional.isPresent()) {
listener = listenerOptional.get();
log.info("Executing button listener {} for event for id {}.", listener.getClass().getSimpleName(), event.getComponentId());
if(listener.autoAcknowledgeEvent()) {
event.deferEdit().queue();
}
listener.execute(model);
InteractionResult result = InteractionResult.fromSuccess();
for (ButtonPostInteractionExecution postInteractionExecution : postInteractionExecutions) {

View File

@@ -0,0 +1,135 @@
package dev.sheldan.abstracto.core.interaction.menu;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.InteractionResult;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListener;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerModel;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.service.FeatureModeService;
import dev.sheldan.abstracto.core.utils.BeanUtils;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component
@Slf4j
public class StringSelectMenuListenerBean extends ListenerAdapter {
@Autowired(required = false)
private List<StringSelectMenuListener> listenerList;
@Autowired
@Qualifier("buttonClickedExecutor")
private TaskExecutor buttonClickedExecutor;
@Autowired
private StringSelectMenuListenerBean self;
@Autowired
private FeatureConfigService featureConfigService;
@Autowired
private FeatureFlagService featureFlagService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private Gson gson;
@Override
public void onStringSelectInteraction(@Nonnull StringSelectInteractionEvent event) {
if(listenerList == null) return;
event.deferEdit().queue();
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), buttonClickedExecutor).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async button event.", throwable);
return null;
});
}
@Transactional
public void executeListenerLogic(StringSelectInteractionEvent event) {
StringSelectMenuListenerModel model = null;
StringSelectMenuListener listener = null;
try {
Optional<ComponentPayload> callbackInformation = componentPayloadManagementService.findPayload(event.getComponentId());
if(callbackInformation.isPresent()) {
model = getModel(event, callbackInformation.get());
List<StringSelectMenuListener> validListener = filterFeatureAwareListener(listenerList, model);
Optional<StringSelectMenuListener> listenerOptional = findListener(validListener, model);
if(listenerOptional.isPresent()) {
listener = listenerOptional.get();
log.info("Executing string select menu listener {} for event for id {}.", listener.getClass().getSimpleName(), event.getComponentId());
listener.execute(model);
} else {
log.warn("No listener found for string select menu event for id {}.", event.getComponentId());
}
} else {
log.warn("No callback found for id {}.", event.getComponentId());
}
} catch (Exception exception) {
if(event.isFromGuild()) {
log.error("String select menu listener failed with exception in server {} and channel {}.", event.getGuild().getIdLong(),
event.getGuildChannel().getIdLong(), exception);
} else {
log.error("String select menu listener failed with exception outside of a guild.", exception);
}
}
}
private Optional<StringSelectMenuListener> findListener(List<StringSelectMenuListener> featureAwareListeners, StringSelectMenuListenerModel model) {
return featureAwareListeners.stream().filter(asyncButtonClickedListener -> asyncButtonClickedListener.handlesEvent(model)).findFirst();
}
private List<StringSelectMenuListener> filterFeatureAwareListener(List<StringSelectMenuListener> featureAwareListeners, StringSelectMenuListenerModel model) {
return featureAwareListeners.stream().filter(trFeatureAwareListener -> {
FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(trFeatureAwareListener.getFeature());
if(!model.getEvent().isFromGuild()) {
return true;
}
if (!featureFlagService.isFeatureEnabled(feature, model.getServerId())) {
return false;
}
return featureModeService.necessaryFeatureModesMet(trFeatureAwareListener, model.getServerId());
}).collect(Collectors.toList());
}
private StringSelectMenuListenerModel getModel(StringSelectInteractionEvent event, ComponentPayload componentPayload) throws ClassNotFoundException {
SelectMenuPayload payload = null;
if(componentPayload.getPayloadType() != null && componentPayload.getPayload() != null) {
payload = (SelectMenuPayload) gson.fromJson(componentPayload.getPayload(), Class.forName(componentPayload.getPayloadType()));
}
return StringSelectMenuListenerModel
.builder()
.event(event)
.deserializedPayload(payload)
.payload(componentPayload.getPayload())
.origin(componentPayload.getOrigin())
.build();
}
@PostConstruct
public void postConstruct() {
BeanUtils.sortPrioritizedListeners(listenerList);
}
}

View File

@@ -7,6 +7,7 @@ import dev.sheldan.abstracto.core.interaction.modal.config.TextInputComponent;
import dev.sheldan.abstracto.core.interaction.modal.config.TextInputComponentStyle;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.interactions.components.text.TextInput;
@@ -34,6 +35,12 @@ public class ModalServiceBean implements ModalService {
return event.replyModal(modal).submit();
}
@Override
public CompletableFuture<Void> replyModal(ButtonInteractionEvent event, String templateKey, Object model) {
Modal modal = createModalFromTemplate(templateKey, model, event.getGuild().getIdLong());
return event.replyModal(modal).submit();
}
@Override
public Modal createModalFromTemplate(String templateKey, Object model, Long serverId) {
String modalConfigString = templateService.renderTemplate(templateKey + "_modal", model, serverId);
@@ -44,7 +51,7 @@ public class ModalServiceBean implements ModalService {
.sorted(Comparator.comparing(ModalComponent::getPosition))
.collect(Collectors.toList());
return Modal.create(modalConfig.getId(), modalConfig.getTitle())
.addActionRows(convertToActionRows(components))
.addComponents(convertToActionRows(components))
.build();
}

View File

@@ -123,12 +123,12 @@ public class SlashCommandParameterServiceBean implements SlashCommandParameterSe
@Override
public Object getCommandOption(String name, SlashCommandInteractionEvent event) {
return event.getOption(name);
return event.getOption(name.toLowerCase(Locale.ROOT));
}
@Override
public Boolean hasCommandOption(String name, SlashCommandInteractionEvent event) {
return event.getOption(name) != null;
return event.getOption(name.toLowerCase(Locale.ROOT)) != null;
}
@Override

View File

@@ -350,7 +350,7 @@ public class ChannelServiceBean implements ChannelService {
.collect(Collectors.toList());
messageAction = messageAction.setFiles(files);
}
messageAction = messageAction.setComponents(messageToSend.getActionRows());
messageAction = messageAction.setComponents(messageToSend.getActionRows()).setReplace(true);
metricService.incrementCounter(MESSAGE_EDIT_METRIC);
return messageAction.submit();
}
@@ -411,6 +411,11 @@ public class ChannelServiceBean implements ChannelService {
});
}
@Override
public CompletableFuture<Message> removeComponents(MessageChannel channel, Long messageId) {
return channel.editMessageComponentsById(messageId, new ArrayList<>()).submit();
}
@Override
public CompletableFuture<Void> deleteTextChannel(AChannel channel) {
return deleteTextChannel(channel.getServer().getId(), channel.getId());

View File

@@ -2,6 +2,7 @@ package dev.sheldan.abstracto.core.service.management;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuConfigModel;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
@@ -59,6 +60,18 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
return createButtonPayload(buttonConfigModel, server);
}
@Override
public ComponentPayload createStringSelectMenuPayload(SelectMenuConfigModel selectMenuConfigModel, Long serverId) {
AServer server = serverManagementService.loadOrCreate(serverId);
return createStringSelectMenuPayload(selectMenuConfigModel, server);
}
@Override
public ComponentPayload createStringSelectMenuPayload(SelectMenuConfigModel selectMenuConfigModel, AServer server) {
String payload = gson.toJson(selectMenuConfigModel.getSelectMenuPayload());
return createPayload(selectMenuConfigModel.getSelectMenuId(), payload, selectMenuConfigModel.getPayloadType(), selectMenuConfigModel.getOrigin(), server, ComponentType.SELECTION);
}
@Override
public ComponentPayload createModalPayload(ModalConfigPayload payloadConfig, Long serverId) {
String payload = gson.toJson(payloadConfig.getModalPayload());

View File

@@ -130,4 +130,27 @@ public class PostTargetManagementBean implements PostTargetManagement {
return postTargetRepository.findByServerReference(server);
}
@Override
public AChannel getPostTarget(Long serverId, String name) {
AServer server = serverManagementService.loadOrCreate(serverId);
return getPostTarget(server, name);
}
@Override
public AChannel getPostTarget(Long serverId, PostTarget target) {
AServer server = serverManagementService.loadOrCreate(serverId);
return getPostTarget(server, target);
}
@Override
public AChannel getPostTarget(AServer server, PostTarget target) {
return target.getChannelReference();
}
@Override
public AChannel getPostTarget(AServer server, String name) {
PostTarget target = getPostTarget(name, server);
return getPostTarget(server, target);
}
}

View File

@@ -20,9 +20,6 @@ public class ServerManagementServiceBean implements ServerManagementService {
@Autowired
private ServerRepository repository;
@Autowired
private PostTargetManagement postTargetManagement;
@Autowired
private UserManagementService userManagementService;
@@ -93,29 +90,6 @@ public class ServerManagementServiceBean implements ServerManagementService {
return aUserInAServer;
}
@Override
public AChannel getPostTarget(Long serverId, String name) {
AServer server = this.loadOrCreate(serverId);
return getPostTarget(server, name);
}
@Override
public AChannel getPostTarget(Long serverId, PostTarget target) {
AServer server = this.loadOrCreate(serverId);
return getPostTarget(server, target);
}
@Override
public AChannel getPostTarget(AServer server, PostTarget target) {
return target.getChannelReference();
}
@Override
public AChannel getPostTarget(AServer server, String name) {
PostTarget target = postTargetManagement.getPostTarget(name, server);
return getPostTarget(server, target);
}
@Override
public List<AServer> getAllServers() {
return repository.findAll();

View File

@@ -17,4 +17,5 @@ public class ButtonConfig {
private String buttonPayload;
private String payloadType;
private ButtonMetaConfig metaConfig;
private Integer position;
}

View File

@@ -22,5 +22,6 @@ public class MessageConfiguration {
private String additionalMessage;
private MetaMessageConfiguration messageConfig;
private List<ButtonConfig> buttons;
private List<SelectionMenuConfig> selectionMenus;
private List<FileConfig> files;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.core.templating.model;
import com.google.gson.annotations.SerializedName;
public enum SelectionMenuChannelType {
@SerializedName("TEXT")
TEXT,
@SerializedName("VOICE")
VOICE
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.core.templating.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
public class SelectionMenuConfig {
private String id;
private SelectionMenuType type;
private List<SelectionMenuTarget> targets;
private List<SelectionMenuChannelType> channelTypes;
private List<SelectionMenuEntry> menuEntries;
private Integer position;
private Integer minValues;
private Integer maxValues;
private Boolean disabled;
private String placeholder;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.core.templating.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class SelectionMenuEntry {
private String value;
private String label;
private Boolean isDefault;
private String description;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.core.templating.model;
import com.google.gson.annotations.SerializedName;
public enum SelectionMenuTarget {
@SerializedName("USER")
USER,
@SerializedName("ROLE")
ROLE,
@SerializedName("CHANNEL")
CHANNEL
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.core.templating.model;
import com.google.gson.annotations.SerializedName;
public enum SelectionMenuType {
@SerializedName("STRING")
STRING,
@SerializedName("ENTITY")
ENTITY
}

View File

@@ -21,10 +21,16 @@ import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -99,54 +105,74 @@ public class TemplateServiceBean implements TemplateService {
convertEmbeds(messageConfiguration, embedBuilders);
}
List<ActionRow> buttons = new ArrayList<>();
List<ActionRow> actionRows = new ArrayList<>();
Map<String, MessageToSend.ComponentConfig> componentPayloads = new HashMap<>();
if(messageConfiguration.getButtons() != null) {
ActionRow currentRow = null;
for (ButtonConfig buttonConfig : messageConfiguration.getButtons()) {
ButtonMetaConfig metaConfig = buttonConfig.getMetaConfig() != null ? buttonConfig.getMetaConfig() : null;
String id = metaConfig != null && Boolean.TRUE.equals(metaConfig.getGenerateRandomUUID()) ?
UUID.randomUUID().toString() : buttonConfig.getId();
String componentOrigin = metaConfig != null ? metaConfig.getButtonOrigin() : null;
MessageToSend.ComponentConfig componentConfig = null;
try {
componentConfig = MessageToSend.ComponentConfig
.builder()
.componentOrigin(componentOrigin)
.componentType(ComponentType.BUTTON)
.persistCallback(metaConfig != null && Boolean.TRUE.equals(metaConfig.getPersistCallback()))
.payload(buttonConfig.getButtonPayload())
.payloadType(buttonConfig.getPayloadType() != null ? Class.forName(buttonConfig.getPayloadType()) : null)
.build();
} catch (ClassNotFoundException e) {
throw new AbstractoRunTimeException("Referenced class in button config could not be found: " + buttonConfig.getPayloadType(), e);
}
componentPayloads.put(id, componentConfig);
String idOrUl = buttonConfig.getUrl() == null ? buttonConfig.getId() : buttonConfig.getUrl();
Button createdButton = Button.of(ButtonStyleConfig.getStyle(buttonConfig.getButtonStyle()), idOrUl, buttonConfig.getLabel());
if (buttonConfig.getDisabled() != null) {
createdButton = createdButton.withDisabled(buttonConfig.getDisabled());
}
if (buttonConfig.getEmoteMarkdown() != null) {
createdButton = createdButton.withEmoji(Emoji.fromFormatted(buttonConfig.getEmoteMarkdown()));
}
if(currentRow == null) {
currentRow = ActionRow.of(createdButton);
} else if (
(
metaConfig != null &&
Boolean.TRUE.equals(metaConfig.getForceNewRow())
)
|| currentRow.getComponents().size() == ComponentServiceBean.MAX_BUTTONS_PER_ROW) {
buttons.add(currentRow);
currentRow = ActionRow.of(createdButton);
if(messageConfiguration.getButtons() != null || messageConfiguration.getSelectionMenus() != null) {
// this basically preprocesses the buttons and select menus
// by getting the positions of the items first
// we only need this, because the current message config does not have them in the same item
// they are two distinct lists, but map to the same concept in discord: components
Set<Integer> positions = new HashSet<>();
HashMap<Integer, ButtonConfig> buttonPositions = new HashMap<>();
List<ButtonConfig> buttonsWithoutPosition = new ArrayList<>();
HashMap<Integer, SelectionMenuConfig> selectionMenuPositions = new HashMap<>();
List<SelectionMenuConfig> selectionMenusWithoutPosition = new ArrayList<>();
// we do this by getting all positions which are part of the config
// we also track which positions are buttons and which are select menus
if(messageConfiguration.getButtons() != null) {
messageConfiguration.getButtons().forEach(buttonConfig -> {
if(buttonConfig.getPosition() != null) {
positions.add(buttonConfig.getPosition());
buttonPositions.put(buttonConfig.getPosition(), buttonConfig);
} else {
buttonsWithoutPosition.add(buttonConfig);
}
});
}
if(messageConfiguration.getSelectionMenus() != null) {
messageConfiguration.getSelectionMenus().forEach(selectionMenuConfig -> {
if(selectionMenuConfig.getPosition() != null) {
positions.add(selectionMenuConfig.getPosition());
selectionMenuPositions.put(selectionMenuConfig.getPosition(), selectionMenuConfig);
} else {
selectionMenusWithoutPosition.add(selectionMenuConfig);
}
});
}
List<Integer> positionsSorted = new ArrayList<>(positions);
Collections.sort(positionsSorted);
List<ButtonConfig> currentButtons = new ArrayList<>();
// we go over all positions, and if its part of the buttons, we only add it to a list of buttons
// this will then mean, that all buttons are processed as a group
// this is necessary, because we can only add buttons as part of an action row
// and in order to make it easier, we process the whole chunk of buttons at once, producing
// at least one or more action rows
for (Integer position : positionsSorted) {
if (buttonPositions.containsKey(position)) {
currentButtons.add(buttonPositions.get(position));
} else {
currentRow.getComponents().add(createdButton);
// if we get interrupted by a selection menu, we process the buttons we have so far
// because those should be handled as a group
// and then process the selection menu, the selection menu will always represent one full action row
// it is not possible to have a button and a menu in the same row
if(!currentButtons.isEmpty()) {
addButtons(actionRows, componentPayloads, currentButtons);
currentButtons.clear();
}
addSelectionMenu(actionRows, selectionMenuPositions.get(position));
}
}
if(currentRow != null) {
buttons.add(currentRow);
if(!currentButtons.isEmpty()) {
addButtons(actionRows, componentPayloads, currentButtons);
currentButtons.clear();
}
// all the rest without positions will be processed at the end (probably default case for most cases)
addButtons(actionRows, componentPayloads, buttonsWithoutPosition);
// selection menus are handled afterwards, that is just implied logic
// to have a select menu before a button, one would need to set accordingly, or only
// set the position for the selection menu, and not for the button
selectionMenusWithoutPosition.forEach(selectionMenuConfig -> addSelectionMenu(actionRows, selectionMenuConfig));
}
setPagingFooters(embedBuilders);
@@ -244,12 +270,129 @@ public class TemplateServiceBean implements TemplateService {
.messages(messages)
.ephemeral(isEphemeral)
.attachedFiles(files)
.actionRows(buttons)
.actionRows(actionRows)
.componentPayloads(componentPayloads)
.referencedMessageId(referencedMessageId)
.build();
}
private void addSelectionMenu(List<ActionRow> actionRows, SelectionMenuConfig selectionMenuConfig) {
ItemComponent selectionMenu;
if (selectionMenuConfig.getType() == SelectionMenuType.STRING) {
List<SelectOption> selectOptions = selectionMenuConfig.getMenuEntries().stream().map(selectionMenuEntry -> {
SelectOption option = SelectOption.of(selectionMenuEntry.getLabel(), selectionMenuEntry.getValue());
if (StringUtils.isNotBlank(selectionMenuEntry.getDescription())) {
option = option.withDescription(selectionMenuEntry.getDescription());
}
if(Boolean.TRUE.equals(selectionMenuEntry.getIsDefault())) {
option = option.withDefault(true);
}
return option;
}).collect(Collectors.toList());
StringSelectMenu.Builder builder = StringSelectMenu
.create(selectionMenuConfig.getId())
.addOptions(selectOptions);
List<SelectOption> defaultOptions = selectOptions
.stream()
.filter(SelectOption::isDefault)
.collect(Collectors.toList());
builder.setDefaultOptions(defaultOptions);
if (selectionMenuConfig.getMaxValues() != null) {
builder.setMaxValues(selectionMenuConfig.getMaxValues());
}
if (selectionMenuConfig.getMinValues() != null) {
builder.setMinValues(selectionMenuConfig.getMinValues());
}
if (selectionMenuConfig.getPlaceholder() != null) {
builder.setPlaceholder(selectionMenuConfig.getPlaceholder());
}
selectionMenu = builder.build();
} else {
Set<EntitySelectMenu.SelectTarget> targets = new HashSet<>();
if(selectionMenuConfig.getTargets() != null) {
selectionMenuConfig.getTargets().forEach(selectionMenuTarget -> {
switch (selectionMenuTarget) {
case ROLE:
targets.add(EntitySelectMenu.SelectTarget.ROLE);
break;
case USER:
targets.add(EntitySelectMenu.SelectTarget.USER);
break;
case CHANNEL:
targets.add(EntitySelectMenu.SelectTarget.CHANNEL);
break;
}
});
}
Set<ChannelType> channelTypes = new HashSet<>();
if(selectionMenuConfig.getChannelTypes() != null) {
selectionMenuConfig.getChannelTypes().forEach(channelType -> {
switch (channelType) {
case TEXT:
channelTypes.add(ChannelType.TEXT);
break;
case VOICE:
channelTypes.add(ChannelType.VOICE);
break;
}
});
}
selectionMenu = EntitySelectMenu.create(selectionMenuConfig.getId(), targets)
.setChannelTypes(channelTypes)
.build();
}
actionRows.add(ActionRow.of(selectionMenu));
}
private void addButtons(List<ActionRow> actionRows, Map<String, MessageToSend.ComponentConfig> componentPayloads, List<ButtonConfig> buttonConfigs) {
ActionRow currentRow = null;
for (ButtonConfig buttonConfig : buttonConfigs) {
ButtonMetaConfig metaConfig = buttonConfig.getMetaConfig() != null ? buttonConfig.getMetaConfig() : null;
String id = metaConfig != null && Boolean.TRUE.equals(metaConfig.getGenerateRandomUUID()) ?
UUID.randomUUID().toString() : buttonConfig.getId();
String componentOrigin = metaConfig != null ? metaConfig.getButtonOrigin() : null;
MessageToSend.ComponentConfig componentConfig = null;
try {
componentConfig = MessageToSend.ComponentConfig
.builder()
.componentOrigin(componentOrigin)
.componentType(ComponentType.BUTTON)
.persistCallback(metaConfig != null && Boolean.TRUE.equals(metaConfig.getPersistCallback()))
.payload(buttonConfig.getButtonPayload())
.payloadType(buttonConfig.getPayloadType() != null ? Class.forName(buttonConfig.getPayloadType()) : null)
.build();
} catch (ClassNotFoundException e) {
throw new AbstractoRunTimeException("Referenced class in button config could not be found: " + buttonConfig.getPayloadType(), e);
}
componentPayloads.put(id, componentConfig);
String idOrUl = buttonConfig.getUrl() == null ? buttonConfig.getId() : buttonConfig.getUrl();
Button createdButton = Button.of(ButtonStyleConfig.getStyle(buttonConfig.getButtonStyle()), idOrUl, buttonConfig.getLabel());
if (buttonConfig.getDisabled() != null) {
createdButton = createdButton.withDisabled(buttonConfig.getDisabled());
}
if (buttonConfig.getEmoteMarkdown() != null) {
createdButton = createdButton.withEmoji(Emoji.fromFormatted(buttonConfig.getEmoteMarkdown()));
}
if(currentRow == null) {
currentRow = ActionRow.of(createdButton);
} else if (
(
metaConfig != null &&
Boolean.TRUE.equals(metaConfig.getForceNewRow())
)
|| currentRow.getComponents().size() == ComponentServiceBean.MAX_BUTTONS_PER_ROW) {
actionRows.add(currentRow);
currentRow = ActionRow.of(createdButton);
} else {
currentRow.getComponents().add(createdButton);
}
}
if(currentRow != null) {
actionRows.add(currentRow);
}
}
private void convertEmbeds(MessageConfiguration messageConfiguration, List<EmbedBuilder> embedBuilders) {
int currentEffectiveEmbed;
for (int embedIndex = 0; embedIndex < messageConfiguration.getEmbeds().size(); embedIndex++) {

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.core.interaction;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuConfigModel;
import dev.sheldan.abstracto.core.interaction.modal.ModalConfigPayload;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
@@ -14,6 +15,8 @@ public interface ComponentPayloadManagementService {
void updatePayload(String id, String payload);
ComponentPayload createButtonPayload(ButtonConfigModel buttonConfigModel, AServer server);
ComponentPayload createButtonPayload(ButtonConfigModel buttonConfigModel, Long serverId);
ComponentPayload createStringSelectMenuPayload(SelectMenuConfigModel selectMenuConfigModel, Long serverId);
ComponentPayload createStringSelectMenuPayload(SelectMenuConfigModel selectMenuConfigModel, AServer server);
ComponentPayload createModalPayload(ModalConfigPayload payloadConfig, Long serverId);
Optional<ComponentPayload> findPayload(String id);
List<ComponentPayload> findPayloadsOfOriginInServer(String buttonOrigin, AServer server);

View File

@@ -12,9 +12,10 @@ public interface InteractionService {
List<CompletableFuture<Message>> sendMessageToInteraction(MessageToSend messageToSend, InteractionHook interactionHook);
List<CompletableFuture<Message>> sendMessageToInteraction(String templateKey, Object model, InteractionHook interactionHook);
CompletableFuture<InteractionHook> replyEmbed(String templateKey, Object model, IReplyCallback callback);
CompletableFuture<InteractionHook> replyString(String text, IReplyCallback callback);
CompletableFuture<InteractionHook> replyString(String text, IReplyCallback callback);
CompletableFuture<InteractionHook> replyEmbed(String templateKey, IReplyCallback callback);
CompletableFuture<Message> editOriginal(MessageToSend messageToSend, InteractionHook interactionHook);
CompletableFuture<InteractionHook> replyMessageToSend(MessageToSend messageToSend, IReplyCallback callback);
CompletableFuture<InteractionHook> replyMessage(String templateKey, Object model, IReplyCallback callback);
CompletableFuture<Message> replyString(String content, InteractionHook interactionHook);
}

View File

@@ -6,4 +6,7 @@ import dev.sheldan.abstracto.core.listener.FeatureAwareListener;
public interface ButtonClickedListener extends FeatureAwareListener<ButtonClickedListenerModel, ButtonClickedListenerResult>, Prioritized, InteractionListener {
Boolean handlesEvent(ButtonClickedListenerModel model);
default Boolean autoAcknowledgeEvent() {
return true;
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.core.interaction.menu;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class SelectMenuConfigModel {
private String selectMenuId;
private SelectMenuPayload selectMenuPayload;
private Class payloadType;
private String origin;
}

View File

@@ -0,0 +1,4 @@
package dev.sheldan.abstracto.core.interaction.menu;
public interface SelectMenuPayload {
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.core.interaction.menu.listener;
import dev.sheldan.abstracto.core.Prioritized;
import dev.sheldan.abstracto.core.interaction.InteractionListener;
import dev.sheldan.abstracto.core.listener.FeatureAwareListener;
public interface StringSelectMenuListener extends FeatureAwareListener<StringSelectMenuListenerModel, StringSelectMenuListenerResult>, Prioritized, InteractionListener {
Boolean handlesEvent(StringSelectMenuListenerModel model);
}

View File

@@ -0,0 +1,24 @@
package dev.sheldan.abstracto.core.interaction.menu.listener;
import dev.sheldan.abstracto.core.interaction.menu.SelectMenuPayload;
import dev.sheldan.abstracto.core.listener.FeatureAwareListenerModel;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
@Getter
@Setter
@Builder
public class StringSelectMenuListenerModel implements FeatureAwareListenerModel {
private StringSelectInteractionEvent event;
private String payload;
private String origin;
private SelectMenuPayload deserializedPayload;
@Override
public Long getServerId() {
return event.isFromGuild() ? event.getGuild().getIdLong() : null;
}
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.abstracto.core.interaction.menu.listener;
import dev.sheldan.abstracto.core.listener.ListenerExecutionResult;
public enum StringSelectMenuListenerResult implements ListenerExecutionResult {
ACKNOWLEDGED, IGNORED
}

View File

@@ -1,11 +1,13 @@
package dev.sheldan.abstracto.core.interaction.modal;
import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.modals.Modal;
import java.util.concurrent.CompletableFuture;
public interface ModalService {
CompletableFuture<Void> replyModal(GenericCommandInteractionEvent event, String templateKey, Object model);
CompletableFuture<Void> replyModal(ButtonInteractionEvent event, String templateKey, Object model);
Modal createModalFromTemplate(String templateKey, Object model, Long serverId);
}

View File

@@ -41,6 +41,7 @@ public interface ChannelService {
CompletableFuture<Message> removeFieldFromMessage(MessageChannel channel, Long messageId, Integer index);
CompletableFuture<Message> editFieldValueInMessage(MessageChannel channel, Long messageId, Integer index, String newValue);
CompletableFuture<Message> removeFieldFromMessage(MessageChannel channel, Long messageId, Integer index, Integer embedIndex);
CompletableFuture<Message> removeComponents(MessageChannel channel, Long messageId);
CompletableFuture<Void> deleteTextChannel(AChannel channel);
CompletableFuture<Void> deleteTextChannel(Long serverId, Long channelId);
List<CompletableFuture<Message>> sendEmbedTemplateInTextChannelList(String templateKey, Object model, MessageChannel channel);

View File

@@ -20,6 +20,10 @@ public interface PostTargetManagement {
Optional<PostTarget> getPostTargetOptional(PostTargetEnum postTargetEnum, Long serverId);
Boolean postTargetExists(String name, AServer server);
boolean postTargetExists(String name, Long serverId);
AChannel getPostTarget(Long serverId, String name);
AChannel getPostTarget(Long serverId, PostTarget target);
AChannel getPostTarget(AServer server, PostTarget target);
AChannel getPostTarget(AServer server, String name);
PostTarget updatePostTarget(PostTarget target, AChannel newTargetChannel);
List<PostTarget> getPostTargetsInServer(AServer server);
}

View File

@@ -15,9 +15,5 @@ public interface ServerManagementService {
void addChannelToServer(AServer server, AChannel channel);
AUserInAServer addUserToServer(AServer server, AUser user);
AUserInAServer addUserToServer(Long serverId, Long userId);
AChannel getPostTarget(Long serverId, String name);
AChannel getPostTarget(Long serverId, PostTarget target);
AChannel getPostTarget(AServer server, PostTarget target);
AChannel getPostTarget(AServer server, String name);
List<AServer> getAllServers();
}