diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/pom.xml b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/pom.xml
index 0ca4779c8..b34c49f6e 100644
--- a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/pom.xml
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/pom.xml
@@ -9,11 +9,6 @@
entertainment-impl
-
- 8
- 8
-
-
@@ -57,6 +52,12 @@
test
+
+ dev.sheldan.abstracto.scheduling
+ scheduling-int
+ ${project.version}
+
+
com.google.code.gson
gson
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/command/PressFCommand.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/command/PressFCommand.java
new file mode 100644
index 000000000..7b54309e2
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/command/PressFCommand.java
@@ -0,0 +1,140 @@
+package dev.sheldan.abstracto.entertainment.command;
+
+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.CommandContext;
+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.service.ChannelService;
+import dev.sheldan.abstracto.core.service.ConfigService;
+import dev.sheldan.abstracto.core.utils.FutureUtils;
+import dev.sheldan.abstracto.core.utils.ParseUtils;
+import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
+import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
+import dev.sheldan.abstracto.entertainment.config.EntertainmentSlashCommandNames;
+import dev.sheldan.abstracto.entertainment.model.command.PressFPromptModel;
+import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
+import net.dv8tion.jda.api.entities.Message;
+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.List;
+import java.util.concurrent.CompletableFuture;
+
+import static dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureConfig.PRESS_F_DEFAULT_DURATION_SECONDS;
+
+@Component
+public class PressFCommand extends AbstractConditionableCommand {
+
+ public static final String TEXT_PARAMETER = "text";
+ public static final String DURATION_PARAMETER = "duration";
+
+ private static final String RESPONSE_TEMPLATE = "pressF_response";
+ public static final String PRESS_F_COMMAND_NAME = "pressF";
+
+ @Autowired
+ private InteractionService interactionService;
+
+ @Autowired
+ private SlashCommandParameterService slashCommandParameterService;
+
+ @Autowired
+ private ConfigService configService;
+
+ @Autowired
+ private EntertainmentService entertainmentService;
+
+ @Autowired
+ private ChannelService channelService;
+
+ @Override
+ public CompletableFuture executeAsync(CommandContext commandContext) {
+ String text = (String) commandContext.getParameters().getParameters().get(0);
+ Long defaultDurationSeconds = configService.getLongValueOrConfigDefault(PRESS_F_DEFAULT_DURATION_SECONDS, commandContext.getGuild().getIdLong());
+ Duration duration = Duration.ofSeconds(defaultDurationSeconds);
+ PressFPromptModel pressFModel = entertainmentService.getPressFModel(text);
+ List> messages = channelService.sendEmbedTemplateInMessageChannelList(RESPONSE_TEMPLATE, pressFModel, commandContext.getChannel());
+ return FutureUtils.toSingleFutureGeneric(messages)
+ .thenAccept(unused -> entertainmentService.persistPressF(text, duration, commandContext.getAuthor(),
+ pressFModel.getPressFComponentId(), commandContext.getChannel(), messages.get(0).join().getIdLong()))
+ .thenApply(unused -> CommandResult.fromSuccess());
+ }
+
+ @Override
+ public CompletableFuture executeSlash(SlashCommandInteractionEvent event) {
+ String text = slashCommandParameterService.getCommandOption(TEXT_PARAMETER, event, String.class);
+ Duration duration;
+ if(slashCommandParameterService.hasCommandOption(DURATION_PARAMETER, event)) {
+ String durationString = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, String.class);
+ duration = ParseUtils.parseDuration(durationString);
+ } else {
+ Long defaultDurationSeconds = configService.getLongValueOrConfigDefault(PRESS_F_DEFAULT_DURATION_SECONDS, event.getGuild().getIdLong());
+ duration = Duration.ofSeconds(defaultDurationSeconds);
+ }
+ PressFPromptModel pressFModel = entertainmentService.getPressFModel(text);
+ return interactionService.replyEmbed(RESPONSE_TEMPLATE, pressFModel, event)
+ .thenCompose(interactionHook -> interactionHook.retrieveOriginal().submit())
+ .thenAccept(message -> {
+ entertainmentService.persistPressF(text, duration, event.getMember(), pressFModel.getPressFComponentId(), event.getGuildChannel(), message.getIdLong());
+ })
+ .thenApply(unused -> CommandResult.fromSuccess());
+ }
+
+ @Override
+ public CommandConfiguration getConfiguration() {
+ List parameters = new ArrayList<>();
+ Parameter textParameter = Parameter
+ .builder()
+ .name(TEXT_PARAMETER)
+ .type(String.class)
+ .templated(true)
+ .remainder(true)
+ .build();
+ parameters.add(textParameter);
+ Parameter durationParameter = Parameter
+ .builder()
+ .name(DURATION_PARAMETER)
+ .type(Duration.class)
+ .slashCommandOnly(true)
+ .optional(true)
+ .templated(true)
+ .build();
+ parameters.add(durationParameter);
+ HelpInfo helpInfo = HelpInfo
+ .builder()
+ .templated(true)
+ .build();
+
+ SlashCommandConfig slashCommandConfig = SlashCommandConfig
+ .builder()
+ .enabled(true)
+ .rootCommandName(EntertainmentSlashCommandNames.ENTERTAINMENT)
+ .commandName("pressf")
+ .build();
+
+ return CommandConfiguration.builder()
+ .name(PRESS_F_COMMAND_NAME)
+ .module(EntertainmentModuleDefinition.ENTERTAINMENT)
+ .templated(true)
+ .causesReaction(false)
+ .async(true)
+ .slashCommandConfig(slashCommandConfig)
+ .supportsEmbedException(true)
+ .parameters(parameters)
+ .help(helpInfo)
+ .build();
+ }
+
+ @Override
+ public FeatureDefinition getFeature() {
+ return EntertainmentFeatureDefinition.ENTERTAINMENT;
+ }
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/job/PressFEvaluationJob.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/job/PressFEvaluationJob.java
new file mode 100644
index 000000000..1b9a7f968
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/job/PressFEvaluationJob.java
@@ -0,0 +1,35 @@
+package dev.sheldan.abstracto.entertainment.job;
+
+import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.JobExecutionContext;
+import org.quartz.PersistJobDataAfterExecution;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.quartz.QuartzJobBean;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@DisallowConcurrentExecution
+@Component
+@PersistJobDataAfterExecution
+public class PressFEvaluationJob extends QuartzJobBean {
+ @Getter
+ @Setter
+ private Long pressFId;
+
+ @Autowired
+ private EntertainmentService entertainmentService;
+
+ @Override
+ protected void executeInternal(JobExecutionContext context) {
+ try {
+ log.info("Executing press f evaluation job for pressf instance {}.", pressFId);
+ entertainmentService.evaluatePressF(pressFId);
+ } catch (Exception exception) {
+ log.error("Press f evaluation job failed.", exception);
+ }
+ }
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/listener/interaction/PressFClickedListener.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/listener/interaction/PressFClickedListener.java
new file mode 100644
index 000000000..4cfc2f3e6
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/listener/interaction/PressFClickedListener.java
@@ -0,0 +1,107 @@
+package dev.sheldan.abstracto.entertainment.listener.interaction;
+
+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.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.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
+import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
+import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
+import dev.sheldan.abstracto.entertainment.exception.AlreadyPressedFException;
+import dev.sheldan.abstracto.entertainment.model.PressFPayload;
+import dev.sheldan.abstracto.entertainment.model.command.PressFJoinModel;
+import dev.sheldan.abstracto.entertainment.model.database.PressF;
+import dev.sheldan.abstracto.entertainment.service.EntertainmentServiceBean;
+import dev.sheldan.abstracto.entertainment.service.management.PressFManagementService;
+import dev.sheldan.abstracto.entertainment.service.management.PressFPresserManagementServiceBean;
+import jakarta.transaction.Transactional;
+import lombok.extern.slf4j.Slf4j;
+import net.dv8tion.jda.api.entities.Member;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@Component
+@Slf4j
+public class PressFClickedListener implements ButtonClickedListener {
+
+ private static final String PRESS_F_CLICK_RESPONSE_TEMPLATE_KEY = "pressF_join";
+
+ @Autowired
+ private PressFPresserManagementServiceBean pressFPresserManagementServiceBean;
+
+ @Autowired
+ private PressFManagementService pressFManagementService;
+
+ @Autowired
+ private UserInServerManagementService userInServerManagementService;
+
+ @Autowired
+ private InteractionService interactionService;
+
+ @Autowired
+ private PressFClickedListener self;
+
+ @Override
+ public Boolean handlesEvent(ButtonClickedListenerModel model) {
+ return EntertainmentServiceBean.PRESS_F_BUTTON_ORIGIN.equals(model.getOrigin());
+ }
+
+ @Override
+ public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
+ PressFPayload payload = (PressFPayload) model.getDeserializedPayload();
+ Optional pressFOptional = pressFManagementService.getPressFById(payload.getPressFId());
+ pressFOptional.ifPresent(pressF -> {
+ Member presserMember = model.getEvent().getMember();
+ AUserInAServer presser = userInServerManagementService.loadOrCreateUser(presserMember);
+ Long userInServerId = presser.getUserInServerId();
+ if(!pressFPresserManagementServiceBean.didUserAlreadyPress(pressF, presser)) {
+ PressFJoinModel joinModel = PressFJoinModel
+ .builder()
+ .messageId(pressF.getMessageId())
+ .memberDisplay(MemberDisplay.fromMember(presserMember))
+ .build();
+ interactionService.replyEmbed(PRESS_F_CLICK_RESPONSE_TEMPLATE_KEY, joinModel, model.getEvent().getInteraction()).thenAccept(interactionHook -> {
+ self.persistPresser(payload.getPressFId(), userInServerId);
+ log.info("Send message about pressing to user {} for pressF {}.", presserMember.getIdLong(), payload.getPressFId());
+ }).exceptionally(throwable -> {
+ log.error("Failed to send message or persist press user {} in pressF {}.", presserMember.getIdLong(), payload.getPressFId(), throwable);
+ return null;
+ });
+ } else {
+ log.debug("User {} already pressed for pressF {}.", presserMember.getIdLong(), payload.getPressFId());
+ throw new AlreadyPressedFException();
+ }
+ });
+ return ButtonClickedListenerResult.ACKNOWLEDGED;
+ }
+
+ @Transactional
+ public void persistPresser(Long pressFId, Long userInServerId) {
+ log.info("Persisting pressing of user {} for pressF {}.", userInServerId, pressFId);
+ AUserInAServer presser = userInServerManagementService.loadOrCreateUser(userInServerId);
+ Optional pressFByIdOptional = pressFManagementService.getPressFById(pressFId);
+ pressFByIdOptional.ifPresent(pressF -> {
+ pressFPresserManagementServiceBean.addPresser(pressF, presser);
+ });
+ }
+
+ @Override
+ public FeatureDefinition getFeature() {
+ return EntertainmentFeatureDefinition.ENTERTAINMENT;
+ }
+
+ @Override
+ public Integer getPriority() {
+ return ListenerPriority.MEDIUM;
+ }
+
+ @Override
+ public Boolean autoAcknowledgeEvent() {
+ return false;
+ }
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/model/PressFPayload.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/model/PressFPayload.java
new file mode 100644
index 000000000..2b36ce854
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/model/PressFPayload.java
@@ -0,0 +1,11 @@
+package dev.sheldan.abstracto.entertainment.model;
+
+import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public class PressFPayload implements ButtonPayload {
+ private Long pressFId;
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFPresserRepository.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFPresserRepository.java
new file mode 100644
index 000000000..e1353332c
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFPresserRepository.java
@@ -0,0 +1,10 @@
+package dev.sheldan.abstracto.entertainment.repository;
+
+import dev.sheldan.abstracto.entertainment.model.database.PressFPresser;
+import dev.sheldan.abstracto.entertainment.model.database.embed.PressFPresserId;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PressFPresserRepository extends JpaRepository {
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFRepository.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFRepository.java
new file mode 100644
index 000000000..8a8cd87e3
--- /dev/null
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/repository/PressFRepository.java
@@ -0,0 +1,9 @@
+package dev.sheldan.abstracto.entertainment.repository;
+
+import dev.sheldan.abstracto.entertainment.model.database.PressF;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PressFRepository extends JpaRepository {
+}
diff --git a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/service/EntertainmentServiceBean.java b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/service/EntertainmentServiceBean.java
index 150f4ef92..3c9c2668c 100644
--- a/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/service/EntertainmentServiceBean.java
+++ b/abstracto-application/abstracto-modules/entertainment/entertainment-impl/src/main/java/dev/sheldan/abstracto/entertainment/service/EntertainmentServiceBean.java
@@ -2,12 +2,33 @@ package dev.sheldan.abstracto.entertainment.service;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
+import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
+import dev.sheldan.abstracto.core.interaction.ComponentPayloadService;
+import dev.sheldan.abstracto.core.interaction.ComponentService;
+import dev.sheldan.abstracto.core.models.database.AChannel;
+import dev.sheldan.abstracto.core.models.database.AUserInAServer;
+import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.ConfigService;
+import dev.sheldan.abstracto.core.service.MessageService;
+import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
+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.entertainment.config.EntertainmentFeatureConfig;
import dev.sheldan.abstracto.entertainment.exception.ReactDuplicateCharacterException;
+import dev.sheldan.abstracto.entertainment.model.PressFPayload;
import dev.sheldan.abstracto.entertainment.model.ReactMapping;
+import dev.sheldan.abstracto.entertainment.model.command.PressFPromptModel;
+import dev.sheldan.abstracto.entertainment.model.command.PressFResultModel;
+import dev.sheldan.abstracto.entertainment.model.database.PressF;
+import dev.sheldan.abstracto.entertainment.service.management.PressFManagementService;
+import dev.sheldan.abstracto.scheduling.model.JobParameters;
+import dev.sheldan.abstracto.scheduling.service.SchedulerService;
+import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
@@ -17,7 +38,10 @@ import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
@@ -30,6 +54,9 @@ public class EntertainmentServiceBean implements EntertainmentService {
"DONT_COUNT", "REPLY_NO", "SOURCES_NO", "OUTLOOK_NOT_GOOD", "DOUBTFUL" // negative
);
+ public static final String PRESS_F_BUTTON_ORIGIN = "PRESS_F_BUTTON";
+ private static final String PRESS_F_RESULT_TEMPLATE_KEY = "pressF_result";
+
private ReactMapping reactMapping;
@Autowired
@@ -38,6 +65,33 @@ public class EntertainmentServiceBean implements EntertainmentService {
@Autowired
private ConfigService configService;
+ @Autowired
+ private ComponentService componentService;
+
+ @Autowired
+ private PressFManagementService pressFManagementService;
+
+ @Autowired
+ private UserInServerManagementService userInServerManagementService;
+
+ @Autowired
+ private SchedulerService schedulerService;
+
+ @Autowired
+ private ComponentPayloadService componentPayloadService;
+
+ @Autowired
+ private ChannelManagementService channelManagementService;
+
+ @Autowired
+ private ChannelService channelService;
+
+ @Autowired
+ private TemplateService templateService;
+
+ @Autowired
+ private MessageService messageService;
+
@Value("classpath:react_mappings.json")
private Resource reactMappingSource;
@@ -91,6 +145,66 @@ public class EntertainmentServiceBean implements EntertainmentService {
return sb.toString();
}
+ @Override
+ public PressFPromptModel getPressFModel(String text) {
+ String pressFComponent = componentService.generateComponentId();
+ return PressFPromptModel
+ .builder()
+ .pressFComponentId(pressFComponent)
+ .text(text)
+ .build();
+ }
+
+ @Transactional
+ public void persistPressF(String text, Duration duration, Member executingMember, String componentId, GuildMessageChannel guildMessageChannel, Long messageId) {
+ Instant targetDate = Instant.now().plus(duration);
+ log.info("Persisting pressF started by {} in server {} with due date {}.", executingMember.getIdLong(), executingMember.getGuild().getIdLong(), targetDate);
+ AUserInAServer creator = userInServerManagementService.loadOrCreateUser(executingMember);
+ AChannel channel = channelManagementService.loadChannel(guildMessageChannel);
+ PressF pressF = pressFManagementService.createPressF(text, targetDate, creator, channel, messageId);
+ HashMap