Compare commits

...

37 Commits

Author SHA1 Message Date
Sheldan
062581fce5 [maven-release-plugin] prepare release v1.5.19 2023-12-26 21:04:45 +01:00
Sheldan
be41551529 [AB-xxx] preparing for release 2023-12-26 21:03:11 +01:00
Sheldan
af8206c529 [AB-xxx] adding bonk image generation command
fixing offline streamer handling not being able to handle streamer without current sessions
2023-12-26 15:42:02 +01:00
Sheldan
8efedf6f6f [AB-xxx] renaming pat sprite png 2023-12-25 23:44:43 +01:00
Sheldan
80aff40054 [AB-xxx] adding pat gif generator command 2023-12-25 23:43:34 +01:00
Sheldan
c71f5f004d [AB-xxx] changing displayed user for already existing user in modmail to be the target user 2023-12-25 12:58:47 +01:00
Sheldan
350a634f50 [maven-release-plugin] prepare for next development iteration 2023-12-25 01:07:26 +01:00
Sheldan
f5de74c1e4 [maven-release-plugin] prepare release v1.5.18 2023-12-25 01:07:21 +01:00
Sheldan
6e3809bfd9 [AB-xxx] prepare for release 2023-12-25 01:06:02 +01:00
Sheldan
8e6339a99b [AB-109] fixing streamers not being marked online if the post target is disabled 2023-12-25 01:00:54 +01:00
Sheldan
befef1f61d [AB-xxx] refactoring modmail to offer a feature mode to use threads instead of channels
adding various utilities for thread channels in core
fixing enable feature not showing validation messages
restructuring feature mode collection to be a startup listener, because postconstruct might not have the appropriate values available, and therefore not initialize the map correctly
2023-12-24 23:25:03 +01:00
Sheldan
1f0bc493d9 [maven-release-plugin] prepare for next development iteration 2023-12-23 23:18:06 +01:00
Sheldan
48fa7a99dc [maven-release-plugin] prepare release v1.5.17 2023-12-23 23:18:02 +01:00
Sheldan
d12497753d [AB-xxx] preparing for release 2023-12-23 23:16:39 +01:00
Sheldan
20a6e29f1b [AB-xxx] fixing custom command alternative to filter when it should be executed more accurately 2023-12-23 23:16:01 +01:00
Sheldan
d2231b0934 [maven-release-plugin] prepare for next development iteration 2023-12-23 21:09:36 +01:00
Sheldan
0464afb7db [maven-release-plugin] prepare release v1.5.16 2023-12-23 21:09:32 +01:00
Sheldan
c473ca74d4 [AB-xxx] prepare for release 2023-12-23 21:08:20 +01:00
Sheldan
a70ac5aa94 [AB-xxx] fixing path for env file loading 2023-12-23 21:05:59 +01:00
Sheldan
d10faf4d3d [maven-release-plugin] prepare for next development iteration 2023-12-23 20:45:33 +01:00
Sheldan
a346e1372c [maven-release-plugin] prepare release v1.5.15 2023-12-23 20:45:28 +01:00
Sheldan
9a7c769e8f [AB-xxx] prepare for release 2023-12-23 20:43:57 +01:00
Sheldan
980ca9380c [AB-70] moving image generation functionality to separate image generation module
removing doge image generation from default base
split rest-api into separate modules (base and extensions)
2023-12-23 20:41:25 +01:00
Sheldan
e9d14ac417 [AB-xxx] refactoring rest-api to not be a maven project and restructuring python tool file structure 2023-12-20 21:05:22 +01:00
Sheldan
474e632fed [AB-xxx] adding rest-api base to abstracto with capability to load custom code if necessary 2023-12-19 01:25:07 +01:00
Sheldan
2e5c2c26b9 [maven-release-plugin] prepare for next development iteration 2023-12-12 19:18:51 +01:00
Sheldan
a8ca9a8055 [maven-release-plugin] prepare release v1.5.14 2023-12-12 19:18:46 +01:00
Sheldan
453ccf2e27 [AB-xxx] prepare for release 2023-12-12 19:17:33 +01:00
Sheldan
52c805f4ea [AB-63] adding pressF command
fixing not setting the won flag for giveaway participants
2023-12-11 01:26:07 +01:00
Sheldan
4b3038078e [maven-release-plugin] prepare for next development iteration 2023-12-10 14:32:10 +01:00
Sheldan
af5f9a8361 [maven-release-plugin] prepare release v1.5.13 2023-12-10 14:32:05 +01:00
Sheldan
e84b5ecbb5 [AB-30] adding giveaway feature 2023-12-10 14:25:08 +01:00
Sheldan
1ba8219d7b [AB-xxx] fixing using wrong parameter for unSuggest command 2023-11-15 01:06:30 +01:00
Sheldan
60d2c669a2 [maven-release-plugin] prepare for next development iteration 2023-10-29 18:23:09 +01:00
Sheldan
3a122a72e5 [maven-release-plugin] prepare release v1.5.12 2023-10-29 18:23:03 +01:00
Sheldan
d53126e5a5 [AB-xxx] actively limiting auto complete responses to the max allowed values
changing interface of slash command parameter service to be more applicable
adding utility functions to slash command auto complete service bean
2023-10-29 18:21:29 +01:00
Sheldan
208d9a28ed [maven-release-plugin] prepare for next development iteration 2023-10-23 01:27:29 +02:00
230 changed files with 6913 additions and 253 deletions

View File

@@ -1,2 +1,2 @@
REGISTRY_PREFIX=harbor.sheldan.dev/abstracto/
VERSION=1.5.11
VERSION=1.5.19

View File

@@ -47,10 +47,9 @@ jobs:
id: dotenv
uses: falti/dotenv-action@v1.0.4
with:
path: ./deployment/installer/.env
path: .env
- name: Push container
run: docker-compose build && docker-compose push
working-directory: ./deployment/installer/
env:
REGISTRY_PREFIX: ${{ steps.dotenv.outputs.registry_prefix }}
VERSION: ${{ steps.dotenv.outputs.version }}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>assignable-roles-int</artifactId>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -45,8 +45,16 @@ public class CustomCommandAlternative implements CommandAlternative {
private CustomCommandFeatureConfig customCommandFeatureConfig;
@Override
public boolean shouldExecute(UnParsedCommandParameter parameter, Guild guild) {
return featureFlagService.isFeatureEnabled(customCommandFeatureConfig, guild.getIdLong());
public boolean shouldExecute(UnParsedCommandParameter parameter, Guild guild, Message message) {
boolean featureEnabled = featureFlagService.isFeatureEnabled(customCommandFeatureConfig, guild.getIdLong());
if(featureEnabled) {
String contentStripped = message.getContentRaw();
List<String> parameters = Arrays.asList(contentStripped.split(" "));
String commandName = commandRegistry.getCommandName(parameters.get(0), message.getGuild().getIdLong());
Optional<CustomCommand> customCommandOptional = customCommandManagementService.getCustomCommandByName(commandName, message.getGuild().getIdLong());
return customCommandOptional.isPresent();
}
return false;
}
@Override

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>dynamic-activity</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>dynamic-activity</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,17 +3,12 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>entertainment-impl</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
@@ -57,6 +52,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>

View File

@@ -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<CommandResult> 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<CompletableFuture<Message>> 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<CommandResult> 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<Parameter> 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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<PressFPresser, PressFPresserId> {
}

View File

@@ -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<PressF, Long> {
}

View File

@@ -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<Object, Object> parameters = new HashMap<>();
parameters.put("pressFId", pressF.getId().toString());
JobParameters jobParameters = JobParameters
.builder()
.parameters(parameters)
.build();
log.debug("Starting scheduled job for pressF {}", pressF.getId());
schedulerService.executeJobWithParametersOnce("pressFEvaluationJob", "entertainment", jobParameters, Date.from(targetDate));
PressFPayload pressFPayload = PressFPayload
.builder()
.pressFId(pressF.getId())
.build();
log.debug("Persisting payload for pressF {}", pressF.getId());
componentPayloadService.createButtonPayload(componentId, pressFPayload, PRESS_F_BUTTON_ORIGIN, creator.getServerReference());
}
@Override
@Transactional
public CompletableFuture<Void> evaluatePressF(Long pressFId) {
Optional<PressF> pressFOptional = pressFManagementService.getPressFById(pressFId);
if(pressFOptional.isPresent()) {
log.info("Evaluating pressF with id {}", pressFId);
PressF pressF = pressFOptional.get();
PressFResultModel model = PressFResultModel
.builder()
.userCount((long) pressF.getPresser().size())
.text(pressF.getText())
.messageId(pressF.getMessageId())
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(PRESS_F_RESULT_TEMPLATE_KEY, model);
Long serverId = pressF.getServer().getId();
Long channelId = pressF.getPressFChannel().getId();
Long messageId = pressF.getMessageId();
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageEmbedToSendToAChannel(messageToSend, pressF.getPressFChannel()))
.thenCompose(unused -> messageService.loadMessage(serverId, channelId, messageId).thenCompose(message -> {
log.info("Clearing buttons from pressF {} in with message {} in channel {} in server {}.", pressFId, pressFId, channelId, serverId);
return componentService.clearButtons(message);
}));
} else {
throw new AbstractoRunTimeException(String.format("PressF with id %s not found.", pressFId));
}
}
@Override
public List<String> convertTextToEmojis(String text) {
return convertTextToEmojis(text, false);

View File

@@ -0,0 +1,36 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.PressF;
import dev.sheldan.abstracto.entertainment.repository.PressFRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Optional;
@Component
public class PressFManagementServiceBean implements PressFManagementService {
@Autowired
private PressFRepository pressFRepository;
@Override
public PressF createPressF(String text, Instant targetDate, AUserInAServer creator, AChannel channel, Long messageId) {
PressF pressF = PressF
.builder()
.server(creator.getServerReference())
.creator(creator)
.messageId(messageId)
.pressFChannel(channel)
.text(text)
.targetDate(targetDate)
.build();
return pressFRepository.save(pressF);
}
@Override
public Optional<PressF> getPressFById(Long pressFId) {
return pressFRepository.findById(pressFId);
}
}

View File

@@ -0,0 +1,32 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.PressF;
import dev.sheldan.abstracto.entertainment.model.database.PressFPresser;
import dev.sheldan.abstracto.entertainment.model.database.embed.PressFPresserId;
import dev.sheldan.abstracto.entertainment.repository.PressFPresserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class PressFPresserManagementServiceBean implements PressFPresserManagementService {
@Autowired
private PressFPresserRepository repository;
@Override
public PressFPresser addPresser(PressF pressF, AUserInAServer presser) {
PressFPresser pressFPresser = PressFPresser
.builder()
.presser(presser)
.id(new PressFPresserId(presser.getUserInServerId(), pressF.getId()))
.build();
return repository.save(pressFPresser);
}
@Override
public boolean didUserAlreadyPress(PressF pressF, AUserInAServer aUserInAServer) {
return repository.existsById(new PressFPresserId(aUserInAServer.getUserInServerId(), pressF.getId()));
}
}

View File

@@ -4,6 +4,9 @@ abstracto.systemConfigs.rouletteBullets.longValue=6
abstracto.systemConfigs.rollDefaultHigh.name=rollDefaultHigh
abstracto.systemConfigs.rollDefaultHigh.longValue=6
abstracto.systemConfigs.pressFDefaultDurationSeconds.name=pressFDefaultDurationSeconds
abstracto.systemConfigs.pressFDefaultDurationSeconds.longValue=90
abstracto.featureFlags.entertainment.featureName=entertainment
abstracto.featureFlags.entertainment.enabled=false

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,20 @@
<?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="entertainmentModule" value="(SELECT id FROM module WHERE name = 'entertainment')"/>
<property name="entertainmentFeature" value="(SELECT id FROM feature WHERE key = 'entertainment')"/>
<changeSet author="Sheldan" id="pressF-commands">
<insert tableName="command">
<column name="name" value="pressF"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${entertainmentFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

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="command.xml" relativeToChangelogFile="true"/>
<include file="press_f_evaluation_job.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,18 @@
<?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="press_f_evaluation_job-insert">
<insert tableName="scheduler_job">
<column name="name" value="pressFEvaluationJob"/>
<column name="group_name" value="entertainment"/>
<column name="clazz" value="dev.sheldan.abstracto.entertainment.job.PressFEvaluationJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
</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="press_f-table">
<createTable tableName="press_f">
<column name="id" autoIncrement="true" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="press_f_pkey"/>
</column>
<column name="creator_user_id" type="INTEGER">
<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="target_date" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="text" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="press_f_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseColumnNames="creator_user_id" baseTableName="press_f" constraintName="fk_press_f_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="press_f" constraintName="fk_press_f_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="press_f_channel_id" baseTableName="press_f" constraintName="fk_press_f_channel"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS press_f_update_trigger ON press_f;
CREATE TRIGGER press_f_update_trigger BEFORE UPDATE ON press_f FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS press_f_insert_trigger ON press_f;
CREATE TRIGGER press_f_insert_trigger BEFORE INSERT ON press_f FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,39 @@
<?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="press_f_presser-table">
<createTable tableName="press_f_presser">
<column name="press_f_presser_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="press_f_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>
<addPrimaryKey columnNames="press_f_presser_user_in_server_id, press_f_id" tableName="press_f_presser" constraintName="pk_press_f_presser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="press_f_presser_user_in_server_id" baseTableName="press_f_presser" constraintName="fk_press_f_presser_user"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id"
referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="press_f_id" baseTableName="press_f_presser" constraintName="fk_press_f_presser_press_f"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id"
referencedTableName="press_f" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS press_f_presser_update_trigger ON press_f_presser;
CREATE TRIGGER press_f_presser_update_trigger BEFORE UPDATE ON press_f_presser FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS press_f_presser_insert_trigger ON press_f_presser;
CREATE TRIGGER press_f_presser_insert_trigger BEFORE INSERT ON press_f_presser FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

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="press_f.xml" relativeToChangelogFile="true"/>
<include file="press_f_presser.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -13,4 +13,5 @@
<include file="1.4.3/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.11/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.8/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.14/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -3,15 +3,10 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>entertainment-int</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -12,6 +12,7 @@ public class EntertainmentFeatureConfig implements FeatureConfig {
public static final String ROULETTE_BULLETS_CONFIG_KEY = "rouletteBullets";
public static final String ROLL_DEFAULT_HIGH_KEY = "rollDefaultHigh";
public static final String PRESS_F_DEFAULT_DURATION_SECONDS = "pressFDefaultDurationSeconds";
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ENTERTAINMENT;
@@ -19,6 +20,6 @@ public class EntertainmentFeatureConfig implements FeatureConfig {
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(ROULETTE_BULLETS_CONFIG_KEY, ROLL_DEFAULT_HIGH_KEY);
return Arrays.asList(ROULETTE_BULLETS_CONFIG_KEY, ROLL_DEFAULT_HIGH_KEY, PRESS_F_DEFAULT_DURATION_SECONDS);
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
public class AlreadyPressedFException extends AbstractoTemplatableException {
@Override
public String getTemplateName() {
return "already_pressed_f_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Builder
@Setter
@Getter
public class PressFJoinModel {
private MemberDisplay memberDisplay;
private Long messageId;
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.entertainment.model.command;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Builder
@Setter
@Getter
public class PressFPromptModel {
private String text;
private String pressFComponentId;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.entertainment.model.command;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Builder
@Setter
@Getter
public class PressFResultModel {
private Long userCount;
private String text;
private Long messageId;
}

View File

@@ -0,0 +1,62 @@
package dev.sheldan.abstracto.entertainment.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 jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "press_f")
@Getter
@Setter
@EqualsAndHashCode
public class PressF {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_user_id", nullable = false)
private AUserInAServer creator;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "press_f_channel_id", nullable = false)
private AChannel pressFChannel;
@Column(name = "message_id", nullable = false)
private Long messageId;
@Column(name = "text")
private String text;
@Column(name = "target_date", nullable = false)
private Instant targetDate;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
@OneToMany(
fetch = FetchType.LAZY,
orphanRemoval = true,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "pressF")
@Builder.Default
private List<PressFPresser> presser = new ArrayList<>();
}

View File

@@ -0,0 +1,38 @@
package dev.sheldan.abstracto.entertainment.model.database;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.embed.PressFPresserId;
import jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "press_f_presser")
@Getter
@Setter
@EqualsAndHashCode
public class PressFPresser {
@EmbeddedId
@Getter
private PressFPresserId id;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
@MapsId("presserId")
@JoinColumn(name = "press_f_presser_user_in_server_id", nullable = false)
private AUserInAServer presser;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(updatable = false, insertable = false, name = "press_f_id", referencedColumnName = "id")
private PressF pressF;
@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,27 @@
package dev.sheldan.abstracto.entertainment.model.database.embed;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import lombok.*;
import java.io.Serializable;
@Embeddable
@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class PressFPresserId implements Serializable {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "press_f_presser_user_in_server_id")
private Long presserId;
@Column(name = "press_f_id")
private Long pressFId;
}

View File

@@ -1,9 +1,13 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.entertainment.exception.ReactDuplicateCharacterException;
import dev.sheldan.abstracto.entertainment.model.command.PressFPromptModel;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface EntertainmentService {
String getEightBallValue(String text);
@@ -12,6 +16,10 @@ public interface EntertainmentService {
boolean executeRoulette(Member memberExecuting);
String takeChoice(List<String> choices, Member memberExecuting);
String createMockText(String text, Member memberExecuting, Member mockedUser);
PressFPromptModel getPressFModel(String text);
void persistPressF(String text, Duration duration, Member executingMember, String componentId, GuildMessageChannel guildMessageChannel, Long messageId);
CompletableFuture<Void> evaluatePressF(Long pressFId);
/**
* Converts the given text to unicode characters (with predefined values from a manual mapping) and returns the matched

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.PressF;
import java.time.Instant;
import java.util.Optional;
public interface PressFManagementService {
PressF createPressF(String text, Instant targetDate, AUserInAServer creator, AChannel channel, Long messageId);
Optional<PressF> getPressFById(Long pressFId);
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.PressF;
import dev.sheldan.abstracto.entertainment.model.database.PressFPresser;
public interface PressFPresserManagementService {
PressFPresser addPresser(PressF pressF, AUserInAServer presser);
boolean didUserAlreadyPress(PressF pressF, AUserInAServer aUserInAServer);
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -23,9 +23,4 @@
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.5.11</version>
<version>1.5.19</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway</artifactId>
<version>1.5.19</version>
</parent>
<artifactId>giveaway-impl</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/liquibase.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,18 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>liquibase</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>.</outputDirectory>
<directory>${project.basedir}/src/main/resources/migrations</directory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,103 @@
package dev.sheldan.abstracto.giveaway.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.CompletableFutureList;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
import jakarta.transaction.Transactional;
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.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class CancelGiveaway extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "cancelGiveaway";
private static final String ID_PARAMETER = "id";
private static final String CANCEL_GIVEAWAY_RESPONSE_TEMPLATE_KEY = "cancelGiveaway_response";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private GiveawayService giveawayService;
@Autowired
private CancelGiveaway self;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
return event.deferReply().submit().thenAccept(interactionHook -> {
self.cancelGiveaway(event);
}).thenCompose(unused -> {
List<CompletableFuture<Message>> futures = interactionService.sendMessageToInteraction(CANCEL_GIVEAWAY_RESPONSE_TEMPLATE_KEY, new Object(), event.getHook());
return new CompletableFutureList<>(futures).getMainFuture();
})
.thenApply(unused -> CommandResult.fromSuccess());
}
@Transactional
public void cancelGiveaway(SlashCommandInteractionEvent event) {
Long giveawayId = slashCommandParameterService.getCommandOption(ID_PARAMETER, event, Integer.class).longValue();
giveawayService.cancelGiveaway(giveawayId, event.getGuild().getIdLong());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter giveawayIdParameter = Parameter
.builder()
.templated(true)
.name(ID_PARAMETER)
.type(Integer.class)
.build();
List<Parameter> parameters = Arrays.asList(giveawayIdParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.commandName("cancel")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
}

View File

@@ -0,0 +1,182 @@
package dev.sheldan.abstracto.giveaway.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.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.model.GiveawayCreationRequest;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
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.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class GreateGiveaway extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "createGiveaway";
private static final String TITLE_PARAMETER = "title";
private static final String DESCRIPTION_PARAMETER = "description";
private static final String BENEFACTOR_PARAMETER = "benefactor";
private static final String CHANNEL_PARAMETER = "channel";
private static final String DURATION_PARAMETER = "duration";
private static final String WINNERS_PARAMETER = "winners";
private static final String CREATE_GIVEAWAY_RESPONSE_TEMPLATE_KEY = "createGiveaway_response";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private GiveawayService giveawayService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
return event.deferReply()
.submit()
.thenCompose(interactionHook -> {
String title = slashCommandParameterService.getCommandOption(TITLE_PARAMETER, event, String.class);
String description;
if(slashCommandParameterService.hasCommandOption(DESCRIPTION_PARAMETER, event)) {
description = slashCommandParameterService.getCommandOption(DESCRIPTION_PARAMETER, event, String.class);
} else {
description = null;
}
String durationString = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, Duration.class, String.class);
Duration duration = ParseUtils.parseDuration(durationString);
GuildMessageChannel target = null;
if(slashCommandParameterService.hasCommandOption(CHANNEL_PARAMETER, event)) {
target = slashCommandParameterService.getCommandOption(CHANNEL_PARAMETER, event, GuildMessageChannel.class);
}
Integer winners = 1;
if(slashCommandParameterService.hasCommandOption(WINNERS_PARAMETER, event)) {
winners = slashCommandParameterService.getCommandOption(WINNERS_PARAMETER, event, Integer.class);
}
Member benefactor;
if(slashCommandParameterService.hasCommandOption(BENEFACTOR_PARAMETER, event)) {
benefactor = slashCommandParameterService.getCommandOption(BENEFACTOR_PARAMETER, event, Member.class);
} else {
benefactor = null;
}
Member creator = event.getMember();
GiveawayCreationRequest request = GiveawayCreationRequest
.builder()
.benefactor(benefactor)
.creator(creator)
.description(description)
.duration(duration)
.targetChannel(target)
.winnerCount(winners)
.title(title)
.build();
return giveawayService.createGiveaway(request)
.thenAccept(unused -> interactionService.sendEmbed(CREATE_GIVEAWAY_RESPONSE_TEMPLATE_KEY, interactionHook));
}).thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter titleParameter = Parameter
.builder()
.templated(true)
.name(TITLE_PARAMETER)
.type(String.class)
.build();
Parameter descriptionParameter = Parameter
.builder()
.templated(true)
.name(DESCRIPTION_PARAMETER)
.type(String.class)
.optional(true)
.build();
Parameter channelParameter = Parameter
.builder()
.name(CHANNEL_PARAMETER)
.type(GuildMessageChannel.class)
.optional(true)
.templated(true)
.build();
Parameter durationParameter = Parameter
.builder()
.name(DURATION_PARAMETER)
.type(Duration.class)
.templated(true)
.build();
Parameter winnersParameter = Parameter
.builder()
.name(WINNERS_PARAMETER)
.type(Integer.class)
.optional(true)
.templated(true)
.build();
Parameter benefactorParameter = Parameter
.builder()
.templated(true)
.name(BENEFACTOR_PARAMETER)
.type(Member.class)
.optional(true)
.build();
List<Parameter> parameters = Arrays.asList(titleParameter, durationParameter, benefactorParameter, descriptionParameter,
channelParameter, winnersParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.commandName("create")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
}

View File

@@ -0,0 +1,103 @@
package dev.sheldan.abstracto.giveaway.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.CompletableFutureList;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
import jakarta.transaction.Transactional;
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.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class ReRollGiveaway extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "reRollGiveaway";
private static final String ID_PARAMETER = "id";
private static final String RE_ROLL_GIVEAWAY_RESPONSE_TEMPLATE_KEY = "reRollGiveaway_response";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private GiveawayService giveawayService;
@Autowired
private ReRollGiveaway self;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
return event.deferReply().submit().thenAccept(interactionHook -> {
self.reRollGiveaway(event);
}).thenCompose(unused -> {
List<CompletableFuture<Message>> futures = interactionService.sendMessageToInteraction(RE_ROLL_GIVEAWAY_RESPONSE_TEMPLATE_KEY, new Object(), event.getHook());
return new CompletableFutureList<>(futures).getMainFuture();
})
.thenApply(unused -> CommandResult.fromSuccess());
}
@Transactional
public void reRollGiveaway(SlashCommandInteractionEvent event) {
Long giveawayId = slashCommandParameterService.getCommandOption(ID_PARAMETER, event, Integer.class).longValue();
giveawayService.evaluateGiveaway(giveawayId, event.getGuild().getIdLong());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter giveawayIdParameter = Parameter
.builder()
.templated(true)
.name(ID_PARAMETER)
.type(Integer.class)
.build();
List<Parameter> parameters = Arrays.asList(giveawayIdParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.commandName("reroll")
.build();
return CommandConfiguration.builder()
.name(COMMAND_NAME)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.giveaway.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:giveaway-config.properties")
public class GiveawayConfig {
}

View File

@@ -0,0 +1,41 @@
package dev.sheldan.abstracto.giveaway.job;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
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 GiveawayEvaluationJob extends QuartzJobBean {
@Getter
@Setter
private Long giveawayId;
@Getter
@Setter
private Long serverId;
@Autowired
private GiveawayService giveawayService;
@Override
protected void executeInternal(JobExecutionContext context) {
try {
log.info("Executing giveaway evaluation job for giveaway {} in server {}", giveawayId, serverId);
giveawayService.evaluateGiveaway(giveawayId, serverId);
} catch (Exception exception) {
log.error("Giveaway evaluation job failed.", exception);
}
}
}

View File

@@ -0,0 +1,94 @@
package dev.sheldan.abstracto.giveaway.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.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.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.exception.GiveawayNotFoundException;
import dev.sheldan.abstracto.giveaway.model.JoinGiveawayPayload;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
import dev.sheldan.abstracto.giveaway.service.GiveawayServiceBean;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayManagementService;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayParticipantManagementService;
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 GiveawayJoinListener implements ButtonClickedListener {
private static final String GIVEAWAY_JOIN_RESPONSE_TEMPLATE_KEY = "giveaway_join_response";
private static final String GIVEAWAY_ALREADY_JOINED_RESPONSE_TEMPLATE_KEY = "giveaway_already_joined_response";
@Autowired
private GiveawayManagementService giveawayManagementService;
@Autowired
private GiveawayService giveawayService;
@Autowired
private InteractionService interactionService;
@Autowired
private GiveawayParticipantManagementService giveawayParticipantManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
JoinGiveawayPayload payload = (JoinGiveawayPayload) model.getDeserializedPayload();
Optional<Giveaway> optionalGiveaway = giveawayManagementService.loadGiveawayById(payload.getGiveawayId(), payload.getServerId());
if(optionalGiveaway.isPresent()) {
Giveaway giveaway = optionalGiveaway.get();
AUserInAServer user = userInServerManagementService.loadOrCreateUser(model.getEvent().getMember());
Long joiningUserId = model.getEvent().getMember().getIdLong();
if(!giveawayParticipantManagementService.userIsAlreadyParticipating(giveaway, user)) {
log.info("Adding user {} to giveaway {}.", joiningUserId, payload.getGiveawayId());
giveawayService.addGiveawayParticipant(giveaway, model.getEvent().getMember(), model.getEvent().getMessageChannel())
.thenAccept(unused -> {
log.info("Notified user {} in giveaway {} join event.", joiningUserId, payload.getGiveawayId());
interactionService.replyEmbed(GIVEAWAY_JOIN_RESPONSE_TEMPLATE_KEY, model.getEvent());
}).exceptionally(throwable -> {
log.error("Failed to add {} to giveaway {}.", joiningUserId, payload.getGiveawayId(), throwable);
return null;
});
} else {
log.info("User {} was already part of giveaway {}.", joiningUserId, payload.getGiveawayId());
interactionService.replyEmbed(GIVEAWAY_ALREADY_JOINED_RESPONSE_TEMPLATE_KEY, model.getEvent());
}
return ButtonClickedListenerResult.ACKNOWLEDGED;
} else {
throw new GiveawayNotFoundException();
}
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
return GiveawayServiceBean.GIVEAWAY_JOIN_ORIGIN.equals(model.getOrigin());
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean autoAcknowledgeEvent() {
return false;
}
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.giveaway.repository;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayParticipant;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayParticipationId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GiveawayParticipantRepository extends JpaRepository<GiveawayParticipant, GiveawayParticipationId> {
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.giveaway.repository;
import dev.sheldan.abstracto.core.models.ServerSpecificId;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GiveawayRepository extends JpaRepository<Giveaway, ServerSpecificId> {
}

View File

@@ -0,0 +1,277 @@
package dev.sheldan.abstracto.giveaway.service;
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.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.CounterService;
import dev.sheldan.abstracto.core.service.PostTargetService;
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.CompletableFutureList;
import dev.sheldan.abstracto.giveaway.config.GiveawayPostTarget;
import dev.sheldan.abstracto.giveaway.exception.GiveawayNotFoundException;
import dev.sheldan.abstracto.giveaway.model.GiveawayCreationRequest;
import dev.sheldan.abstracto.giveaway.model.JoinGiveawayPayload;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayParticipant;
import dev.sheldan.abstracto.giveaway.model.template.GiveawayMessageModel;
import dev.sheldan.abstracto.giveaway.model.template.GiveawayResultMessageModel;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayManagementService;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayParticipantManagementService;
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.Message;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class GiveawayServiceBean implements GiveawayService {
private static final String GIVEAWAY_MESSAGE_TEMPLATE_KEY = "giveaway_post";
private static final String GIVEAWAY_RESULT_MESSAGE_TEMPLATE_KEY = "giveaway_result";
public static final String GIVEAWAY_JOIN_ORIGIN = "JOIN_GIVEAWAY";
public static final String GIVEAWAY_COUNTER = "giveaways";
@Autowired
private ChannelManagementService channelManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private GiveawayManagementService giveawayManagementService;
@Autowired
private ComponentService componentService;
@Autowired
private PostTargetService postTargetService;
@Autowired
private ChannelService channelService;
@Autowired
private TemplateService templateService;
@Autowired
private ComponentPayloadService componentPayloadService;
@Autowired
private GiveawayParticipantManagementService giveawayParticipantManagementService;
@Autowired
private SchedulerService schedulerService;
@Autowired
private SecureRandom secureRandom;
@Autowired
private CounterService counterService;
@Autowired
private GiveawayServiceBean self;
@Override
public CompletableFuture<Void> createGiveaway(GiveawayCreationRequest giveawayCreationRequest) {
String componentId = componentService.generateComponentId();
Instant targetDate = Instant.now().plus(giveawayCreationRequest.getDuration());
Long serverId = giveawayCreationRequest.getCreator().getGuild().getIdLong();
Long giveawayId = counterService.getNextCounterValue(serverId, GIVEAWAY_COUNTER);
GiveawayMessageModel model = GiveawayMessageModel
.builder()
.title(giveawayCreationRequest.getTitle())
.description(giveawayCreationRequest.getDescription())
.giveawayId(giveawayId)
.benefactor(giveawayCreationRequest.getBenefactor() != null ? MemberDisplay.fromMember(giveawayCreationRequest.getBenefactor()) : null)
.creator(MemberDisplay.fromMember(giveawayCreationRequest.getCreator()))
.winnerCount(giveawayCreationRequest.getWinnerCount())
.targetDate(targetDate)
.joinComponentId(componentId)
.build();
List<CompletableFuture<Message>> messageFutures;
log.info("Rendering giveaway message in server {} by user {}", serverId, giveawayCreationRequest.getCreator().getIdLong());
MessageToSend messageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, model, serverId);
if(giveawayCreationRequest.getTargetChannel() == null) {
log.info("Sending giveaway to post target in server {}", serverId);
postTargetService.validatePostTarget(GiveawayPostTarget.GIVEAWAYS, giveawayCreationRequest.getCreator().getGuild().getIdLong());
messageFutures = postTargetService.sendEmbedInPostTarget(messageToSend, GiveawayPostTarget.GIVEAWAYS, serverId);
} else {
log.info("Sending giveaway to channel {} in server {}.", giveawayCreationRequest.getTargetChannel().getId(), serverId);
messageFutures = channelService.sendMessageToSendToChannel(messageToSend, giveawayCreationRequest.getTargetChannel());
}
CompletableFutureList<Message> messageFutureList = new CompletableFutureList<>(messageFutures);
return messageFutureList.getMainFuture().thenAccept(o -> {
Message createdMessage = messageFutureList.getFutures().get(0).join();
giveawayCreationRequest.setTargetChannel(createdMessage.getGuildChannel());
self.persistGiveaway(giveawayCreationRequest, giveawayId, createdMessage.getIdLong(), componentId);
});
}
@Override
public CompletableFuture<Void> addGiveawayParticipant(Giveaway giveaway, Member member, MessageChannel messageChannel) {
GiveawayMessageModel giveawayMessageModel = GiveawayMessageModel.fromGiveaway(giveaway);
giveawayMessageModel.setJoinedUserCount(giveaway.getParticipants().size() + 1L);
Long giveawayId = giveaway.getGiveawayId().getId();
log.info("Adding giveaway participating of user {} to giveaway {} in server {}.", member.getIdLong(), giveawayId, member.getGuild().getIdLong());
MessageToSend messageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, giveawayMessageModel);
return channelService.editEmbedMessageInAChannel(messageToSend.getEmbeds().get(0), messageChannel, giveaway.getMessageId())
.thenAccept(message -> {
self.persistAddedParticipant(member, giveawayId);
});
}
@Override
@Transactional
public CompletableFuture<Void> evaluateGiveaway(Long giveawayId, Long serverId) {
Optional<Giveaway> giveAwayOptional = giveawayManagementService.loadGiveawayById(giveawayId, serverId);
if(giveAwayOptional.isEmpty()) {
throw new GiveawayNotFoundException();
}
log.info("Evaluating giveaway {} in server {}.", giveawayId, serverId);
Giveaway giveaway = giveAwayOptional.get();
Set<Long> winners = new HashSet<>();
Integer winnerCount = giveaway.getWinnerCount();
giveaway.getParticipants().forEach(giveawayParticipant -> giveawayParticipant.setWon(false));
List<Long> potentialWinners = new ArrayList<>(giveaway
.getParticipants()
.stream()
.map(giveawayParticipant -> giveawayParticipant.getParticipant().getUserInServerId())
.toList());
if(potentialWinners.size() <= winnerCount) {
winners.addAll(potentialWinners);
log.debug("Less participants than total winners - selecting all for giveaway {} in server {}.", giveawayId, serverId);
} else {
for (int i = 0; i < winnerCount; i++) {
int winnerIndex = secureRandom.nextInt(potentialWinners.size());
Long winner = potentialWinners.get(winnerIndex);
potentialWinners.remove(winnerIndex);
winners.add(winner);
}
}
List<GiveawayParticipant> winningParticipants = giveaway
.getParticipants()
.stream()
.filter(giveawayParticipant -> winners.contains(giveawayParticipant.getParticipant().getUserInServerId()))
.toList();
winningParticipants.forEach(giveawayParticipant -> giveawayParticipant.setWon(true));
List<MemberDisplay> winnerDisplays = winningParticipants
.stream()
.map(giveawayParticipant -> MemberDisplay.fromAUserInAServer(giveawayParticipant.getParticipant()))
.toList();
GiveawayResultMessageModel resultModel = GiveawayResultMessageModel
.builder()
.messageId(giveaway.getMessageId())
.title(giveaway.getTitle())
.winners(winnerDisplays)
.build();
log.info("Sending result message for giveaway {} in server {}.", giveawayId, serverId);
MessageToSend messageToSend = templateService.renderEmbedTemplate(GIVEAWAY_RESULT_MESSAGE_TEMPLATE_KEY, resultModel);
List<CompletableFuture<Message>> resultFutures = channelService.sendMessageEmbedToSendToAChannel(messageToSend, giveaway.getGiveawayChannel());
GiveawayMessageModel giveawayMessageModel = GiveawayMessageModel.fromGiveaway(giveaway);
giveawayMessageModel.setWinners(winnerDisplays);
giveawayMessageModel.setEnded(true);
MessageToSend giveawayMessageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, giveawayMessageModel);
log.info("Updating original giveaway message for giveaway {} in server {}.", giveawayId, serverId);
GuildMessageChannel messageChannel = channelService.getMessageChannelFromServer(giveaway.getServer().getId(), giveaway.getGiveawayChannel().getId());
CompletableFuture<Message> giveawayUpdateFuture = channelService.editMessageInAChannelFuture(giveawayMessageToSend, messageChannel, giveaway.getMessageId());
resultFutures.add(giveawayUpdateFuture);
return new CompletableFutureList<>(resultFutures).getMainFuture();
}
@Override
public CompletableFuture<Void> cancelGiveaway(Long giveawayId, Long serverId) {
Optional<Giveaway> giveAwayOptional = giveawayManagementService.loadGiveawayById(giveawayId, serverId);
if(giveAwayOptional.isEmpty()) {
throw new GiveawayNotFoundException();
}
Giveaway giveaway = giveAwayOptional.get();
log.info("Cancelling giveaway with id {} in server {}.", giveawayId, serverId);
GiveawayMessageModel giveawayMessageModel = GiveawayMessageModel.fromGiveaway(giveaway);
giveawayMessageModel.setCancelled(true);
schedulerService.stopTrigger(giveaway.getReminderTriggerKey());
MessageToSend giveawayMessageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, giveawayMessageModel);
GuildMessageChannel messageChannel = channelService.getMessageChannelFromServer(giveaway.getServer().getId(), giveaway.getGiveawayChannel().getId());
log.debug("Updating original giveaway message to consider cancellation for giveaway {} in server {}.", giveawayId, serverId);
return channelService.editMessageInAChannelFuture(giveawayMessageToSend, messageChannel, giveaway.getMessageId())
.thenAccept(message -> {
self.persistGiveawayCancellation(giveawayId, serverId);
});
}
@Transactional
public void persistGiveawayCancellation(Long giveawayId, Long serverId) {
Optional<Giveaway> giveAwayOptional = giveawayManagementService.loadGiveawayById(giveawayId, serverId);
if(giveAwayOptional.isEmpty()) {
throw new GiveawayNotFoundException();
}
log.info("Persisting cancellation of giveaway {} in server {}.", giveawayId, serverId);
Giveaway giveaway = giveAwayOptional.get();
giveaway.setCancelled(true);
}
@Transactional
public void persistAddedParticipant(Member member, Long giveawayId) {
log.info("Storing user {} as participant to giveaway {} in server {}.", member.getIdLong(), giveawayId, member.getGuild().getIdLong());
Optional<Giveaway> giveAwayOptional = giveawayManagementService.loadGiveawayById(giveawayId, member.getGuild().getIdLong());
giveAwayOptional.ifPresent(giveaway -> {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
giveawayParticipantManagementService.addParticipant(giveaway, aUserInAServer);
});
}
@Transactional
public void persistGiveaway(GiveawayCreationRequest giveawayCreationRequest, Long giveawayId, Long messageId, String componentId) {
Member creatorMember = giveawayCreationRequest.getCreator();
log.info("Persisting giveaway in server {} with message id {}.", creatorMember.getGuild().getIdLong(), messageId);
Instant targetDate = Instant.now().plus(giveawayCreationRequest.getDuration());
AChannel targetChannel = channelManagementService.loadChannel(giveawayCreationRequest.getTargetChannel().getIdLong());
AUserInAServer creator = userInServerManagementService.loadOrCreateUser(creatorMember);
AUserInAServer benefactor;
if(giveawayCreationRequest.getBenefactor() != null) {
benefactor = userInServerManagementService.loadOrCreateUser(giveawayCreationRequest.getBenefactor());
} else {
benefactor = null;
}
Giveaway giveaway = giveawayManagementService.createGiveaway(creator, benefactor, targetChannel, targetDate,
giveawayCreationRequest.getTitle(), giveawayCreationRequest.getDescription(), giveawayCreationRequest.getWinnerCount(),
messageId, componentId, giveawayId);
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("giveawayId", giveaway.getGiveawayId().getId().toString());
parameters.put("serverId", giveaway.getGiveawayId().getServerId().toString());
JobParameters jobParameters = JobParameters
.builder()
.parameters(parameters)
.build();
log.info("Scheduling giveaway reminder for giveaway {} originating from message {} in server {}.", giveaway.getGiveawayId().getId(), messageId, creatorMember.getGuild().getIdLong());
String triggerKey = schedulerService.executeJobWithParametersOnce("giveawayEvaluationJob", "giveaway", jobParameters, Date.from(giveaway.getTargetDate()));
giveaway.setReminderTriggerKey(triggerKey);
JoinGiveawayPayload joinPayload = JoinGiveawayPayload
.builder()
.giveawayId(giveaway.getGiveawayId().getId())
.serverId(giveaway.getGiveawayId().getServerId())
.build();
componentPayloadService.createButtonPayload(componentId, joinPayload, GIVEAWAY_JOIN_ORIGIN, creator.getServerReference());
}
}

View File

@@ -0,0 +1,46 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.core.models.ServerSpecificId;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import dev.sheldan.abstracto.giveaway.repository.GiveawayRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Optional;
@Component
public class GiveawayManagementServiceBean implements GiveawayManagementService {
@Autowired
private GiveawayRepository giveawayRepository;
@Override
public Giveaway createGiveaway(AUserInAServer creator, AUserInAServer benefactor, AChannel target,
Instant targetDate, String title, String description, Integer winnerCount,
Long messageId, String componentId, Long giveawayId) {
Giveaway giveaway = Giveaway
.builder()
.giveawayId(new ServerSpecificId(creator.getServerReference().getId(), giveawayId))
.creator(creator)
.benefactor(benefactor)
.messageId(messageId)
.componentId(componentId)
.server(creator.getServerReference())
.winnerCount(winnerCount)
.cancelled(false)
.title(title)
.giveawayChannel(target)
.description(description)
.targetDate(targetDate)
.build();
return giveawayRepository.save(giveaway);
}
@Override
public Optional<Giveaway> loadGiveawayById(Long giveawayId, Long serverId) {
return giveawayRepository.findById(new ServerSpecificId(serverId, giveawayId));
}
}

View File

@@ -0,0 +1,34 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayParticipant;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayParticipationId;
import dev.sheldan.abstracto.giveaway.repository.GiveawayParticipantRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class GiveawayParticipantManagementServiceBean implements GiveawayParticipantManagementService {
@Autowired
private GiveawayParticipantRepository repository;
@Override
public void addParticipant(Giveaway giveaway, AUserInAServer aUserInAServer) {
GiveawayParticipationId id = new GiveawayParticipationId(aUserInAServer.getUserInServerId(), giveaway.getGiveawayId().getId(), giveaway.getServer().getId());
GiveawayParticipant participant = GiveawayParticipant
.builder()
.id(id)
.giveaway(giveaway)
.participant(aUserInAServer)
.won(false)
.build();
repository.save(participant);
}
@Override
public boolean userIsAlreadyParticipating(Giveaway giveaway, AUserInAServer aUserInAServer) {
return repository.existsById(new GiveawayParticipationId(aUserInAServer.getUserInServerId(), giveaway.getGiveawayId().getId(), giveaway.getServer().getId()));
}
}

View File

@@ -0,0 +1,4 @@
abstracto.featureFlags.giveaway.featureName=giveaway
abstracto.featureFlags.giveaway.enabled=false
abstracto.postTargets.giveaways.name=giveaways

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="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,29 @@
<?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="giveawayFeature" value="(SELECT id FROM feature WHERE key = 'giveaway')"/>
<changeSet author="Sheldan" id="giveaway-commands">
<insert tableName="command">
<column name="name" value="createGiveaway"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="reRollGiveaway"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="cancelGiveaway"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</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="giveaway_evaluation_job.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="giveaway_feature-insertion">
<insert tableName="feature">
<column name="key" value="giveaway"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,18 @@
<?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="giveaway_evaluation_job-insert">
<insert tableName="scheduler_job">
<column name="name" value="giveawayEvaluationJob"/>
<column name="group_name" value="giveaway"/>
<column name="clazz" value="dev.sheldan.abstracto.giveaway.job.GiveawayEvaluationJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,75 @@
<?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="giveaway-table">
<createTable tableName="giveaway">
<column name="id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="creator_user_id" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="benefactor_user_id" type="INTEGER">
<constraints nullable="true"/>
</column>
<column name="title" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="component_id" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="reminder_trigger_key" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="giveaway_channel_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="message_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="cancelled" type="BOOLEAN">
<constraints nullable="false"/>
</column>
<column name="target_date" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="winner_count" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="server_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>
<addPrimaryKey tableName="giveaway" columnNames="id, server_id"/>
<addForeignKeyConstraint baseColumnNames="creator_user_id" baseTableName="giveaway" constraintName="fk_giveaway_creator" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id"
referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="benefactor_user_id" baseTableName="giveaway" constraintName="fk_giveaway_benefactor" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id"
referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="giveaway_channel_id" baseTableName="giveaway" constraintName="fk_giveaway_channel" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="channel" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="giveaway" constraintName="fk_giveaway_server" deferrable="false" initiallyDeferred="false"
onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS giveaway_update_trigger ON giveaway;
CREATE TRIGGER giveaway_update_trigger BEFORE UPDATE ON giveaway FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS giveaway_insert_trigger ON giveaway;
CREATE TRIGGER giveaway_insert_trigger BEFORE INSERT ON giveaway FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,45 @@
<?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="giveaway_participant-table">
<createTable tableName="giveaway_participant">
<column name="participant_user_in_server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="giveaway_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="won" type="BOOLEAN">
<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>
<addPrimaryKey columnNames="participant_user_in_server_id, giveaway_id, server_id" tableName="giveaway_participant" constraintName="pk_giveaway_participant" validate="true"/>
<addForeignKeyConstraint baseColumnNames="participant_user_in_server_id" baseTableName="giveaway_participant" constraintName="fk_giveaway_participant_participant"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id"
referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="giveaway_id, server_id" baseTableName="giveaway_participant" constraintName="fk_giveaway_participant_giveaway"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id, server_id"
referencedTableName="giveaway" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS giveaway_participant_update_trigger ON giveaway_participant;
CREATE TRIGGER giveaway_participant_update_trigger BEFORE UPDATE ON giveaway_participant FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS giveaway_participant_insert_trigger ON giveaway_participant;
CREATE TRIGGER giveaway_participant_insert_trigger BEFORE INSERT ON giveaway_participant FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

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="giveaway.xml" relativeToChangelogFile="true"/>
<include file="giveaway_participant.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?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="1.5.13/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>giveaway</artifactId>
<version>1.5.19</version>
</parent>
<artifactId>giveaway-int</artifactId>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.giveaway.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class GiveawayFeatureConfig implements FeatureConfig {
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
@Override
public List<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(GiveawayPostTarget.GIVEAWAYS);
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.config;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import lombok.Getter;
@Getter
public enum GiveawayFeatureDefinition implements FeatureDefinition {
GIVEAWAY("giveaway");
private String key;
GiveawayFeatureDefinition(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.config;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import lombok.Getter;
@Getter
public enum GiveawayPostTarget implements PostTargetEnum {
GIVEAWAYS("giveaways");
private String key;
GiveawayPostTarget(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,5 @@
package dev.sheldan.abstracto.giveaway.config;
public class GiveawaySlashCommandNames {
public static final String GIVEAWAY = "giveaway";
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
public class GiveawayNotFoundException extends AbstractoTemplatableException {
@Override
public String getTemplateName() {
return "giveaway_not_found_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,23 @@
package dev.sheldan.abstracto.giveaway.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import java.time.Duration;
@Builder
@Getter
public class GiveawayCreationRequest {
private Member creator;
private Member benefactor;
private String title;
private String description;
private Duration duration;
@Setter
private GuildMessageChannel targetChannel;
private Integer winnerCount;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.giveaway.model;
import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class JoinGiveawayPayload implements ButtonPayload {
private Long giveawayId;
private Long serverId;
}

View File

@@ -0,0 +1,82 @@
package dev.sheldan.abstracto.giveaway.model.database;
import dev.sheldan.abstracto.core.models.ServerSpecificId;
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 jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "giveaway")
@Getter
@Setter
@EqualsAndHashCode
public class Giveaway {
@Id
@EmbeddedId
private ServerSpecificId giveawayId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_user_id", nullable = false)
private AUserInAServer creator;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "benefactor_user_id")
private AUserInAServer benefactor;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "description")
private String description;
@Column(name = "component_id")
private String componentId;
@Column(name = "winner_count", nullable = false)
private Integer winnerCount;
@Column(name = "target_date", nullable = false)
private Instant targetDate;
@Column(name = "cancelled", nullable = false)
private Boolean cancelled;
@Column(name = "reminder_trigger_key")
private String reminderTriggerKey;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "giveaway_channel_id", nullable = false)
private AChannel giveawayChannel;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@MapsId("serverId")
@JoinColumn(name = "server_id", referencedColumnName = "id", nullable = false)
private AServer server;
@Column(name = "message_id", nullable = false)
private Long messageId;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
@OneToMany(
fetch = FetchType.LAZY,
orphanRemoval = true,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "giveaway")
@Builder.Default
private List<GiveawayParticipant> participants = new ArrayList<>();
}

View File

@@ -0,0 +1,45 @@
package dev.sheldan.abstracto.giveaway.model.database;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayParticipationId;
import jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "giveaway_participant")
@Getter
@Setter
@EqualsAndHashCode
public class GiveawayParticipant {
@EmbeddedId
@Getter
private GiveawayParticipationId id;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
@MapsId("participantId")
@JoinColumn(name = "participant_user_in_server_id", nullable = false)
private AUserInAServer participant;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns(
{
@JoinColumn(updatable = false, insertable = false, name = "giveaway_id", referencedColumnName = "id"),
@JoinColumn(updatable = false, insertable = false, name = "server_id", referencedColumnName = "server_id")
})
private Giveaway giveaway;
@Column(name = "won", nullable = false)
private Boolean won;
@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,26 @@
package dev.sheldan.abstracto.giveaway.model.database.embed;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.*;
import java.io.Serializable;
@Embeddable
@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class GiveawayParticipationId implements Serializable {
@Column(name = "participant_user_in_server_id")
private Long participantId;
@Column(name = "giveaway_id")
private Long giveawayId;
@Column(name = "server_id")
private Long serverId;
}

View File

@@ -0,0 +1,48 @@
package dev.sheldan.abstracto.giveaway.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Builder
@Getter
@Setter
public class GiveawayMessageModel {
private String title;
private String description;
private Integer winnerCount;
private Long giveawayId;
@Builder.Default
private Boolean ended = false;
@Builder.Default
private Boolean cancelled = false;
@Builder.Default
private Long joinedUserCount = 0L;
private MemberDisplay creator;
private MemberDisplay benefactor;
private Instant targetDate;
private String joinComponentId;
@Builder.Default
private List<MemberDisplay> winners = new ArrayList<>();
public static GiveawayMessageModel fromGiveaway(Giveaway giveaway) {
return GiveawayMessageModel
.builder()
.title(giveaway.getTitle())
.description(giveaway.getDescription())
.benefactor(giveaway.getBenefactor() != null ? MemberDisplay.fromAUserInAServer(giveaway.getBenefactor()) : null)
.creator(MemberDisplay.fromAUserInAServer(giveaway.getCreator()))
.winnerCount(giveaway.getWinnerCount())
.joinedUserCount((long) giveaway.getParticipants().size())
.joinComponentId(giveaway.getComponentId())
.giveawayId(giveaway.getGiveawayId().getId())
.targetDate(giveaway.getTargetDate())
.build();
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class GiveawayResultMessageModel {
private String title;
private Long messageId;
private List<MemberDisplay> winners;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.service;
import dev.sheldan.abstracto.giveaway.model.GiveawayCreationRequest;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.concurrent.CompletableFuture;
public interface GiveawayService {
CompletableFuture<Void> createGiveaway(GiveawayCreationRequest giveawayCreationRequest);
CompletableFuture<Void> addGiveawayParticipant(Giveaway giveaway, Member member, MessageChannel messageChannel);
CompletableFuture<Void> evaluateGiveaway(Long giveawayId, Long serverId);
CompletableFuture<Void> cancelGiveaway(Long giveawayId, Long serverId);
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
import java.time.Instant;
import java.util.Optional;
public interface GiveawayManagementService {
Giveaway createGiveaway(AUserInAServer creator, AUserInAServer benefactor, AChannel target,
Instant targetDate, String title, String description, Integer winnerCount, Long messageId,
String componentId, Long giveawayId);
Optional<Giveaway> loadGiveawayById(Long giveawayId, Long serverId);
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.Giveaway;
public interface GiveawayParticipantManagementService {
void addParticipant(Giveaway giveaway, AUserInAServer aUserInAServer);
boolean userIsAlreadyParticipating(Giveaway giveaway, AUserInAServer aUserInAServer);
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.5.19</version>
</parent>
<artifactId>giveaway</artifactId>
<packaging>pom</packaging>
<modules>
<module>giveaway-int</module>
<module>giveaway-impl</module>
</modules>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>core-int</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>image-generation</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.5.19</version>
</parent>
<artifactId>image-generation-impl</artifactId>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/liquibase.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>image-generation-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,18 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>liquibase</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<outputDirectory>.</outputDirectory>
<directory>${project.basedir}/src/main/resources/migrations</directory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,147 @@
package dev.sheldan.abstracto.imagegeneration.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.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.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames;
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Bonk extends AbstractConditionableCommand {
public static final String MEMBER_PARAMETER_KEY = "member";
@Autowired
private ImageGenerationService imageGenerationService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Autowired
private FileService fileService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
private static final String BONK_EMBED_TEMPLATE_KEY = "bonk_response";
@Value("${abstracto.feature.imagegeneration.bonk.imagesize}")
private Integer imageSize;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member member;
List<Object> parameters = commandContext.getParameters().getParameters();
if(parameters.isEmpty()) {
member = commandContext.getAuthor();
} else {
member = (Member) parameters.get(0);
}
File bonkGifFile = imageGenerationService.getBonkGif(member.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(bonkGifFile)
.fileName("bonk.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member targetMember;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) {
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class);
} else {
targetMember = event.getMember();
}
File bonkGifFile = imageGenerationService.getBonkGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(bonkGifFile)
.fileName("bonk.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_KEY)
.type(Member.class)
.templated(true)
.optional(true)
.build();
parameters.add(memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION)
.groupName("memes")
.commandName("bonk")
.build();
return CommandConfiguration.builder()
.name("bonk")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -0,0 +1,147 @@
package dev.sheldan.abstracto.imagegeneration.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.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.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames;
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Pat extends AbstractConditionableCommand {
public static final String MEMBER_PARAMETER_KEY = "member";
@Autowired
private ImageGenerationService imageGenerationService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Autowired
private FileService fileService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
private static final String PAT_EMBED_TEMPLATE_KEY = "pat_response";
@Value("${abstracto.feature.imagegeneration.pat.imagesize}")
private Integer imageSize;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member member;
List<Object> parameters = commandContext.getParameters().getParameters();
if(parameters.isEmpty()) {
member = commandContext.getAuthor();
} else {
member = (Member) parameters.get(0);
}
File patGifFile = imageGenerationService.getPatGif(member.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(PAT_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(patGifFile)
.fileName("pat.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member targetMember;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) {
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class);
} else {
targetMember = event.getMember();
}
File patGifFile = imageGenerationService.getPatGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(PAT_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(patGifFile)
.fileName("pat.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_KEY)
.type(Member.class)
.templated(true)
.optional(true)
.build();
parameters.add(memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION)
.groupName("memes")
.commandName("pat")
.build();
return CommandConfiguration.builder()
.name("pat")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -0,0 +1,147 @@
package dev.sheldan.abstracto.imagegeneration.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.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.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition;
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames;
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Triggered extends AbstractConditionableCommand {
public static final String MEMBER_PARAMETER_KEY = "member";
@Autowired
private ImageGenerationService imageGenerationService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Autowired
private FileService fileService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
private static final String TRIGGERED_EMBED_TEMPLATE_KEY = "triggered_response";
@Value("${abstracto.feature.imagegeneration.triggered.imagesize}")
private Integer imageSize;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member member;
List<Object> parameters = commandContext.getParameters().getParameters();
if(parameters.isEmpty()) {
member = commandContext.getAuthor();
} else {
member = (Member) parameters.get(0);
}
File triggeredGifFile = imageGenerationService.getTriggeredGif(member.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(TRIGGERED_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(triggeredGifFile)
.fileName("triggered.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Member targetMember;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) {
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class);
} else {
targetMember = event.getMember();
}
File triggeredGifFile = imageGenerationService.getTriggeredGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(TRIGGERED_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(triggeredGifFile)
.fileName("triggered.gif")
.build();
messageToSend.getAttachedFiles().add(file);
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
.thenApply(unused -> CommandResult.fromIgnored());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER_KEY)
.type(Member.class)
.templated(true)
.optional(true)
.build();
parameters.add(memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION)
.groupName("memes")
.commandName("triggered")
.build();
return CommandConfiguration.builder()
.name("triggered")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.imagegeneration.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:image-generation-config.properties")
public class ImageGenerationConfig {
}

View File

@@ -0,0 +1,55 @@
package dev.sheldan.abstracto.imagegeneration.service;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.service.HttpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
@Component
public class ImageGenerationServiceBean implements ImageGenerationService {
@Value("${abstracto.feature.imagegeneration.triggered.url}")
private String triggeredUrl;
@Value("${abstracto.feature.imagegeneration.pat.url}")
private String patUrl;
@Value("${abstracto.feature.imagegeneration.bonk.url}")
private String bonkUrl;
@Autowired
private HttpService httpService;
@Override
public File getTriggeredGif(String imageUrl) {
try {
return httpService.downloadFileToTempFile(triggeredUrl.replace("{1}", imageUrl));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download triggered gif for url %s with error %s", imageUrl, e.getMessage()));
}
}
@Override
public File getPatGif(String imageUrl) {
try {
return httpService.downloadFileToTempFile(patUrl.replace("{1}", imageUrl));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download pat gif for url %s with error %s", imageUrl, e.getMessage()));
}
}
@Override
public File getBonkGif(String imageUrl) {
try {
return httpService.downloadFileToTempFile(bonkUrl.replace("{1}", imageUrl));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download bonk gif for url %s with error %s", imageUrl, e.getMessage()));
}
}
}

View File

@@ -0,0 +1,11 @@
abstracto.featureFlags.imageGeneration.featureName=imageGeneration
abstracto.featureFlags.imageGeneration.enabled=false
abstracto.feature.imagegeneration.triggered.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/triggered/file.gif?url={1}
abstracto.feature.imagegeneration.triggered.imagesize=128
abstracto.feature.imagegeneration.pat.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/pat/file.gif?url={1}
abstracto.feature.imagegeneration.pat.imagesize=128
abstracto.feature.imagegeneration.bonk.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/bonk/file.gif?url={1}
abstracto.feature.imagegeneration.bonk.imagesize=128

View File

@@ -0,0 +1,10 @@
<?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"/>
</databaseChangeLog>

Some files were not shown because too many files have changed in this diff Show More