[AB-xxx] adding ability to automate giveaways using modmail and adding storage for give away keys

ignoring message updated events for messages not within servers
This commit is contained in:
Sheldan
2024-12-25 19:54:24 +01:00
parent 8819c12b81
commit cdbdb395ac
38 changed files with 1266 additions and 87 deletions

View File

@@ -38,6 +38,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>modmail-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>

View File

@@ -0,0 +1,136 @@
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.config.FeatureMode;
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.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawayMode;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayKeyManagementService;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
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.stereotype.Component;
@Component
public class AddGiveawayKey extends AbstractConditionableCommand {
private static final String KEY_PARAMETER = "key";
private static final String DESCRIPTION_PARAMETER = "description";
private static final String BENEFACTOR_PARAMETER = "benefactor";
private static final String NAME_PARAMETER = "name";
private static final String ADD_GIVEAWAY_KEY_COMMAND_NAME = "addGiveawayKey";
private static final String ADD_GIVEAWAY_KEY_RESPONSE = "addGiveawayKey_response";
@Autowired
private InteractionService interactionService;
@Autowired
private GiveawayKeyManagementService giveawayKeyManagementService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String key = slashCommandParameterService.getCommandOption(KEY_PARAMETER, event, String.class);
String name = slashCommandParameterService.getCommandOption(NAME_PARAMETER, event, String.class);
String description;
if(slashCommandParameterService.hasCommandOption(DESCRIPTION_PARAMETER, event)) {
description = slashCommandParameterService.getCommandOption(DESCRIPTION_PARAMETER, event, String.class);
} else {
description = null;
}
Member benefactor;
if(slashCommandParameterService.hasCommandOption(BENEFACTOR_PARAMETER, event)) {
benefactor = slashCommandParameterService.getCommandOption(BENEFACTOR_PARAMETER, event, Member.class);
} else {
benefactor = null;
}
giveawayKeyManagementService.createGiveawayKey(event.getMember(), benefactor, key, description, name);
return interactionService.replyEmbed(ADD_GIVEAWAY_KEY_RESPONSE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter giveawayKey = Parameter
.builder()
.templated(true)
.name(KEY_PARAMETER)
.type(String.class)
.build();
Parameter giveawayKeyDescription = Parameter
.builder()
.templated(true)
.name(DESCRIPTION_PARAMETER)
.type(String.class)
.optional(true)
.build();
Parameter giveawayKeyBenefactor = Parameter
.builder()
.templated(true)
.name(BENEFACTOR_PARAMETER)
.type(Member.class)
.optional(true)
.build();
Parameter giveawayKeyName = Parameter
.builder()
.templated(true)
.name(NAME_PARAMETER)
.type(String.class)
.build();
List<Parameter> parameters = Arrays.asList(giveawayKeyName, giveawayKey, giveawayKeyBenefactor, giveawayKeyDescription);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("keys")
.commandName("add")
.build();
return CommandConfiguration.builder()
.name(ADD_GIVEAWAY_KEY_COMMAND_NAME)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandOnly(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(GiveawayMode.KEY_GIVEAWAYS);
}
}

View File

@@ -80,6 +80,7 @@ public class CancelGiveaway extends AbstractConditionableCommand {
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("management")
.commandName("cancel")
.build();

View File

@@ -18,6 +18,7 @@ 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 net.dv8tion.jda.api.interactions.InteractionHook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -25,6 +26,7 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.springframework.transaction.annotation.Transactional;
@Component
public class GreateGiveaway extends AbstractConditionableCommand {
@@ -48,55 +50,62 @@ public class GreateGiveaway extends AbstractConditionableCommand {
@Autowired
private GiveawayService giveawayService;
@Autowired
private GreateGiveaway self;
@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;
}
.thenCompose(interactionHook -> self.createGiveaway(event, interactionHook))
.thenApply(unused -> CommandResult.fromSuccess());
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);
}
@Transactional
public CompletableFuture<Void> createGiveaway(SlashCommandInteractionEvent event, InteractionHook 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;
}
Integer winners = 1;
if(slashCommandParameterService.hasCommandOption(WINNERS_PARAMETER, event)) {
winners = slashCommandParameterService.getCommandOption(WINNERS_PARAMETER, event, Integer.class);
}
String durationString = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, Duration.class, String.class);
Duration duration = ParseUtils.parseDuration(durationString);
Member benefactor;
if(slashCommandParameterService.hasCommandOption(BENEFACTOR_PARAMETER, event)) {
benefactor = slashCommandParameterService.getCommandOption(BENEFACTOR_PARAMETER, event, Member.class);
} else {
benefactor = null;
}
GuildMessageChannel target = null;
if(slashCommandParameterService.hasCommandOption(CHANNEL_PARAMETER, event)) {
target = slashCommandParameterService.getCommandOption(CHANNEL_PARAMETER, event, GuildMessageChannel.class);
}
Member creator = event.getMember();
GiveawayCreationRequest request = GiveawayCreationRequest
.builder()
.benefactor(benefactor)
.creator(creator)
.description(description)
.duration(duration)
.targetChannel(target)
.winnerCount(winners)
.title(title)
.build();
Integer winners = 1;
if(slashCommandParameterService.hasCommandOption(WINNERS_PARAMETER, event)) {
winners = slashCommandParameterService.getCommandOption(WINNERS_PARAMETER, event, Integer.class);
}
return giveawayService.createGiveaway(request)
.thenAccept(unused -> interactionService.sendEmbed(CREATE_GIVEAWAY_RESPONSE_TEMPLATE_KEY, interactionHook));
}).thenApply(unused -> CommandResult.fromSuccess());
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()
.benefactorId(benefactor != null ? benefactor.getIdLong() : null)
.creatorId(creator.getIdLong())
.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));
}
@Override
@@ -159,6 +168,7 @@ public class GreateGiveaway extends AbstractConditionableCommand {
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("management")
.commandName("create")
.build();

View File

@@ -0,0 +1,160 @@
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.config.FeatureMode;
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.ConfigService;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureConfig;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawayMode;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.exception.GiveawayNotPossibleException;
import dev.sheldan.abstracto.giveaway.exception.GiveawayKeyNotFoundException;
import dev.sheldan.abstracto.giveaway.model.GiveawayCreationRequest;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayKey;
import dev.sheldan.abstracto.giveaway.service.GiveawayService;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayKeyManagementService;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class GreateKeyGiveaway extends AbstractConditionableCommand {
private static final String COMMAND_NAME = "createKeyGiveaway";
private static final String KEY_ID_PARAMETER = "keyId";
private static final String DURATION_PARAMETER = "duration";
private static final String CREATE_KEY_GIVEAWAY_RESPONSE_TEMPLATE_KEY = "createKeyGiveaway_response";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private GiveawayService giveawayService;
@Autowired
private GiveawayKeyManagementService giveawayKeyManagementService;
@Autowired
private ConfigService configService;
@Autowired
private GreateKeyGiveaway self;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Long id = slashCommandParameterService.getCommandOption(KEY_ID_PARAMETER, event, Integer.class).longValue();
String durationString;
if(slashCommandParameterService.hasCommandOption(DURATION_PARAMETER, event)) {
durationString = slashCommandParameterService.getCommandOption(DURATION_PARAMETER, event, Duration.class, String.class);
} else {
durationString = configService.getStringValueOrConfigDefault(GiveawayFeatureConfig.KEY_GIVEAWAYS_DURATION, event.getGuild().getIdLong()).trim();
}
Duration duration = ParseUtils.parseDuration(durationString);
return event.deferReply()
.submit()
.thenCompose(interactionHook -> self.createKeyGiveaway(id, interactionHook, duration))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Transactional
public CompletableFuture<Void> createKeyGiveaway(Long giveawayKeyId, InteractionHook interactionHook, Duration duration) {
Long serverId = interactionHook.getInteraction().getGuild().getIdLong();
GiveawayKey giveawayKey = giveawayKeyManagementService.getById(giveawayKeyId, serverId)
.orElseThrow(GiveawayKeyNotFoundException::new);
if((giveawayKey.getGiveaway() != null && giveawayKey.getGiveaway().getTargetDate().isAfter(Instant.now())) || giveawayKey.getUsed()) {
throw new GiveawayNotPossibleException();
}
GiveawayCreationRequest request = GiveawayCreationRequest
.builder()
.benefactorId(giveawayKey.getBenefactor() != null ? giveawayKey.getBenefactor().getUserReference().getId() : null)
.creatorId(giveawayKey.getCreator().getUserReference().getId())
.description(giveawayKey.getDescription())
.duration(duration)
.serverId(serverId)
.giveawayKeyId(giveawayKeyId)
.winnerCount(1)
.title(giveawayKey.getName())
.build();
return giveawayService.createGiveaway(request)
.thenAccept(giveawayId -> {
interactionService.sendEmbed(CREATE_KEY_GIVEAWAY_RESPONSE_TEMPLATE_KEY, interactionHook);
});
}
@Override
public CommandConfiguration getConfiguration() {
Parameter keyParameter = Parameter
.builder()
.templated(true)
.name(KEY_ID_PARAMETER)
.type(Long.class)
.build();
Parameter durationParameter = Parameter
.builder()
.templated(true)
.name(DURATION_PARAMETER)
.optional(true)
.type(Duration.class)
.build();
List<Parameter> parameters = Arrays.asList(keyParameter, durationParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("key")
.commandName("creategiveaway")
.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;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(GiveawayMode.KEY_GIVEAWAYS);
}
}

View File

@@ -80,6 +80,7 @@ public class ReRollGiveaway extends AbstractConditionableCommand {
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("management")
.commandName("reroll")
.build();

View File

@@ -0,0 +1,96 @@
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.config.FeatureMode;
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.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawayMode;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayKeyManagementService;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RemoveGiveawayKey extends AbstractConditionableCommand {
private static final String ID_PARAMETER = "id";
private static final String REMOVE_GIVEAWAY_KEY_COMMAND_NAME = "removeGiveawayKey";
private static final String REMOVE_GIVEAWAY_KEY_RESPONSE = "removeGiveawayKey_response";
@Autowired
private GiveawayKeyManagementService giveawayKeyManagementService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private InteractionService interactionService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Long id = slashCommandParameterService.getCommandOption(ID_PARAMETER, event, Integer.class).longValue();
giveawayKeyManagementService.deleteById(id, event.getGuild().getIdLong());
return interactionService.replyEmbed(REMOVE_GIVEAWAY_KEY_RESPONSE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter giveawayKeyId = Parameter
.builder()
.templated(true)
.name(ID_PARAMETER)
.type(Long.class)
.build();
List<Parameter> parameters = Arrays.asList(giveawayKeyId);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("keys")
.commandName("remove")
.build();
return CommandConfiguration.builder()
.name(REMOVE_GIVEAWAY_KEY_COMMAND_NAME)
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandOnly(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(GiveawayMode.KEY_GIVEAWAYS);
}
}

View File

@@ -0,0 +1,140 @@
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.config.FeatureMode;
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.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.PaginatorService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.giveaway.config.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawayMode;
import dev.sheldan.abstracto.giveaway.config.GiveawaySlashCommandNames;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayKey;
import dev.sheldan.abstracto.giveaway.model.template.GiveawayKeyDisplayModel;
import dev.sheldan.abstracto.giveaway.model.template.GiveawayKeysDisplayModel;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayKeyManagementService;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ShowGiveawayKeys extends AbstractConditionableCommand {
public static final String SHOW_ALL_PARAMETER_NAME = "all";
private static final String SHOW_GIVEAWAY_KEYS_RESPONSE_TEMPLATE = "showGiveawayKeys_response";
private static final String SHOW_GIVEAWAY_NO_KEYS_FOUND_TEMPLATE = "showGiveawayKeys_no_keys_found";
@Autowired
private PaginatorService paginatorService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private GiveawayKeyManagementService giveawayKeyManagementService;
@Autowired
private TemplateService templateService;
@Autowired
private InteractionService interactionService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
boolean showAll;
if(slashCommandParameterService.hasCommandOption(SHOW_ALL_PARAMETER_NAME, event)) {
showAll = slashCommandParameterService.getCommandOption(SHOW_ALL_PARAMETER_NAME, event, Boolean.class);
} else {
showAll = false;
}
List<GiveawayKey> giveawayKeys = giveawayKeyManagementService.getGiveawayKeys(event.getGuild().getIdLong(), showAll);
if(giveawayKeys.isEmpty()) {
MessageToSend messageToSend = templateService.renderEmbedTemplate(SHOW_GIVEAWAY_NO_KEYS_FOUND_TEMPLATE, new Object(), event.getGuild().getIdLong());
return interactionService.replyMessageToSend(messageToSend, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
List<GiveawayKeyDisplayModel> models = giveawayKeys
.stream()
.map(giveawayKey -> GiveawayKeyDisplayModel
.builder()
.key(giveawayKey.getKey())
.id(giveawayKey.getId().getKeyId())
.used(giveawayKey.getUsed())
.description(giveawayKey.getDescription())
.name(giveawayKey.getName())
.winner(giveawayKey.getWinner() != null ? MemberDisplay.fromAUserInAServer(giveawayKey.getWinner()) : null)
.creator(MemberDisplay.fromAUserInAServer(giveawayKey.getCreator()))
.benefactor(giveawayKey.getBenefactor() != null ? MemberDisplay.fromAUserInAServer(giveawayKey.getBenefactor()) : null)
.build())
.toList();
GiveawayKeysDisplayModel model = GiveawayKeysDisplayModel
.builder()
.keys(models)
.build();
return paginatorService.createPaginatorFromTemplate(SHOW_GIVEAWAY_KEYS_RESPONSE_TEMPLATE, model, event)
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
Parameter showAllParameter = Parameter
.builder()
.templated(true)
.name(SHOW_ALL_PARAMETER_NAME)
.type(Boolean.class)
.optional(true)
.build();
List<Parameter> parameters = Arrays.asList(showAllParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(GiveawaySlashCommandNames.GIVEAWAY)
.groupName("keys")
.commandName("show")
.build();
return CommandConfiguration.builder()
.name("showGiveawayKeys")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.slashCommandConfig(slashCommandConfig)
.async(true)
.slashCommandOnly(true)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
}
@Override
public List<FeatureMode> getFeatureModeLimitations() {
return Arrays.asList(GiveawayMode.KEY_GIVEAWAYS);
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.giveaway.repository;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayKey;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayKeyId;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GiveawayKeyRepository extends JpaRepository<GiveawayKey, GiveawayKeyId> {
List<GiveawayKey> findGiveawayKeysByUsedAndServer_IdOrderById(Boolean used, Long serverId);
List<GiveawayKey> findGiveawayKeysByServer_IdOrderById(Long serverId);
}

View File

@@ -7,28 +7,38 @@ 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.FeatureModeService;
import dev.sheldan.abstracto.core.service.PostTargetService;
import dev.sheldan.abstracto.core.service.UserService;
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.GiveawayFeatureDefinition;
import dev.sheldan.abstracto.giveaway.config.GiveawayMode;
import dev.sheldan.abstracto.giveaway.config.GiveawayPostTarget;
import dev.sheldan.abstracto.giveaway.exception.GiveawayKeyNotFoundException;
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.GiveawayKey;
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.model.template.GiveawayWinnerNotificationMessageModel;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayKeyManagementService;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayManagementService;
import dev.sheldan.abstracto.giveaway.service.management.GiveawayParticipantManagementService;
import dev.sheldan.abstracto.modmail.service.ModMailThreadService;
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.User;
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;
@@ -44,6 +54,7 @@ import java.util.concurrent.CompletableFuture;
public class GiveawayServiceBean implements GiveawayService {
private static final String GIVEAWAY_MESSAGE_TEMPLATE_KEY = "giveaway_post";
private static final String GIVEAWAY_WINNER_MODMAIL_NOTIFICATION = "giveaway_winner_modmail_notification";
private static final String GIVEAWAY_RESULT_MESSAGE_TEMPLATE_KEY = "giveaway_result";
public static final String GIVEAWAY_JOIN_ORIGIN = "JOIN_GIVEAWAY";
@@ -85,42 +96,55 @@ public class GiveawayServiceBean implements GiveawayService {
@Autowired
private CounterService counterService;
@Autowired
private GiveawayKeyManagementService giveawayKeyManagementService;
@Autowired
private UserService userService;
@Autowired
private FeatureModeService featureModeService;
@Autowired(required = false)
private ModMailThreadService modMailThreadService;
@Autowired
private GiveawayServiceBean self;
@Override
public CompletableFuture<Void> createGiveaway(GiveawayCreationRequest giveawayCreationRequest) {
public CompletableFuture<Long> createGiveaway(GiveawayCreationRequest giveawayCreationRequest) {
String componentId = componentService.generateComponentId();
Instant targetDate = Instant.now().plus(giveawayCreationRequest.getDuration());
Long serverId = giveawayCreationRequest.getCreator().getGuild().getIdLong();
Long serverId = giveawayCreationRequest.getServerId();
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()))
.benefactor(giveawayCreationRequest.getBenefactorId() != null ? MemberDisplay.fromIds(giveawayCreationRequest.getServerId(), giveawayCreationRequest.getBenefactorId()) : null)
.creator(MemberDisplay.fromIds(giveawayCreationRequest.getServerId(), giveawayCreationRequest.getCreatorId()))
.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());
log.info("Rendering giveaway message in server {} by user {}", serverId, giveawayCreationRequest.getCreatorId());
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());
postTargetService.validatePostTarget(GiveawayPostTarget.GIVEAWAYS, serverId);
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 -> {
return messageFutureList.getMainFuture().thenApply(o -> {
Message createdMessage = messageFutureList.getFutures().get(0).join();
giveawayCreationRequest.setTargetChannel(createdMessage.getGuildChannel());
self.persistGiveaway(giveawayCreationRequest, giveawayId, createdMessage.getIdLong(), componentId);
return giveawayId;
});
}
@@ -140,13 +164,9 @@ public class GiveawayServiceBean implements GiveawayService {
@Override
@Transactional
public CompletableFuture<Void> evaluateGiveaway(Long giveawayId, Long serverId) {
Optional<Giveaway> giveAwayOptional = giveawayManagementService.loadGiveawayById(giveawayId, serverId);
if(giveAwayOptional.isEmpty()) {
throw new GiveawayNotFoundException();
}
Giveaway giveaway = giveawayManagementService.loadGiveawayById(giveawayId, serverId).orElseThrow(GiveawayNotFoundException::new);
log.info("Evaluating giveaway {} in server {}.", giveawayId, serverId);
Giveaway giveaway = giveAwayOptional.get();
Set<Long> winners = new HashSet<>();
Set<Long> winnerUserInServerIds = new HashSet<>();
Integer winnerCount = giveaway.getWinnerCount();
giveaway.getParticipants().forEach(giveawayParticipant -> giveawayParticipant.setWon(false));
List<Long> potentialWinners = new ArrayList<>(giveaway
@@ -156,20 +176,20 @@ public class GiveawayServiceBean implements GiveawayService {
.toList());
if(potentialWinners.size() <= winnerCount) {
winners.addAll(potentialWinners);
winnerUserInServerIds.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);
winnerUserInServerIds.add(winner);
}
}
List<GiveawayParticipant> winningParticipants = giveaway
.getParticipants()
.stream()
.filter(giveawayParticipant -> winners.contains(giveawayParticipant.getParticipant().getUserInServerId()))
.filter(giveawayParticipant -> winnerUserInServerIds.contains(giveawayParticipant.getParticipant().getUserInServerId()))
.toList();
winningParticipants.forEach(giveawayParticipant -> giveawayParticipant.setWon(true));
List<MemberDisplay> winnerDisplays = winningParticipants
@@ -185,16 +205,55 @@ public class GiveawayServiceBean implements GiveawayService {
log.info("Sending result message for giveaway {} in server {}.", giveawayId, serverId);
MessageToSend messageToSend = templateService.renderEmbedTemplate(GIVEAWAY_RESULT_MESSAGE_TEMPLATE_KEY, resultModel, serverId);
List<CompletableFuture<Message>> resultFutures = channelService.sendMessageEmbedToSendToAChannel(messageToSend, giveaway.getGiveawayChannel());
long actualWinnerCount = winnerUserInServerIds.size();
Long winnerUserId;
if(giveaway.getGiveawayKey() != null && !winningParticipants.isEmpty()) {
GiveawayParticipant winnerParticipant = winningParticipants.get(0);
GiveawayKey giveawayKey = giveaway.getGiveawayKey();
giveawayKey.setWinner(winnerParticipant.getParticipant());
giveawayKey.setUsed(true);
winnerUserId = winnerParticipant.getParticipant().getUserReference().getId();
} else {
winnerUserId = null;
}
GiveawayMessageModel giveawayMessageModel = GiveawayMessageModel.fromGiveaway(giveaway);
giveawayMessageModel.setWinners(winnerDisplays);
giveawayMessageModel.setEnded(true);
boolean createGiveawayKeyNotification = giveaway.getGiveawayKey() != null
&& featureModeService.featureModeActive(GiveawayFeatureDefinition.GIVEAWAY, serverId, GiveawayMode.AUTO_NOTIFY_GIVEAWAY_KEY_WINNERS)
&& featureModeService.featureModeActive(GiveawayFeatureDefinition.GIVEAWAY, serverId, GiveawayMode.KEY_GIVEAWAYS)
&& actualWinnerCount > 0
&& modMailThreadService != null;
MessageToSend giveawayMessageToSend = templateService.renderEmbedTemplate(GIVEAWAY_MESSAGE_TEMPLATE_KEY, giveawayMessageModel, serverId);
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();
return new CompletableFutureList<>(resultFutures).getMainFuture().thenCompose(unused -> {
if(createGiveawayKeyNotification) {
return userService.retrieveUserForId(winnerUserId)
.thenCompose(user -> self.handleKeyGiveawayNotifications(giveawayId, serverId, winnerUserInServerIds.iterator().next(), user))
.exceptionally(throwable -> {
log.error("Failed to notify winner of giveaway {} in server {}.", giveawayId, serverId, throwable);
return null;
});
} else {
return CompletableFuture.completedFuture(null);
}
});
}
@Transactional
public CompletableFuture<Void> handleKeyGiveawayNotifications(Long giveawayId, Long serverId, Long winnerInServerId, User user) {
if(modMailThreadService == null) {
log.info("Modmail service not available - skipping notifications about giveaway {} in server {}.", giveawayId, serverId);
return CompletableFuture.completedFuture(null);
}
Giveaway giveaway = giveawayManagementService.loadGiveawayById(giveawayId, serverId).orElseThrow(GiveawayNotFoundException::new);
GiveawayWinnerNotificationMessageModel messageModel = GiveawayWinnerNotificationMessageModel.fromGiveaway(giveaway, giveaway.getGiveawayKey().getKey());
AUserInAServer winner = userInServerManagementService.loadOrCreateUser(winnerInServerId);
MessageToSend giveawayWinnerNotification = templateService.renderEmbedTemplate(GIVEAWAY_WINNER_MODMAIL_NOTIFICATION, messageModel, serverId);
return modMailThreadService.sendMessageToUser(winner, giveawayWinnerNotification, user);
}
@Override
@@ -241,14 +300,13 @@ public class GiveawayServiceBean implements GiveawayService {
@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);
log.info("Persisting giveaway in server {} with message id {}.", giveawayCreationRequest.getServerId(), messageId);
Instant targetDate = Instant.now().plus(giveawayCreationRequest.getDuration());
AChannel targetChannel = channelManagementService.loadChannel(giveawayCreationRequest.getTargetChannel().getIdLong());
AUserInAServer creator = userInServerManagementService.loadOrCreateUser(creatorMember);
AUserInAServer creator = userInServerManagementService.loadOrCreateUser(giveawayCreationRequest.getServerId(), giveawayCreationRequest.getCreatorId());
AUserInAServer benefactor;
if(giveawayCreationRequest.getBenefactor() != null) {
benefactor = userInServerManagementService.loadOrCreateUser(giveawayCreationRequest.getBenefactor());
if(giveawayCreationRequest.getBenefactorId() != null) {
benefactor = userInServerManagementService.loadOrCreateUser(giveawayCreationRequest.getServerId(), giveawayCreationRequest.getBenefactorId());
} else {
benefactor = null;
}
@@ -257,6 +315,12 @@ public class GiveawayServiceBean implements GiveawayService {
giveawayCreationRequest.getTitle(), giveawayCreationRequest.getDescription(), giveawayCreationRequest.getWinnerCount(),
messageId, componentId, giveawayId);
if(giveawayCreationRequest.getGiveawayKeyId() != null) {
GiveawayKey giveawayKey = giveawayKeyManagementService.getById(giveawayCreationRequest.getGiveawayKeyId(), giveawayCreationRequest.getServerId())
.orElseThrow(GiveawayKeyNotFoundException::new);
giveawayKey.setGiveaway(giveaway);
giveawayKeyManagementService.saveGiveawayKey(giveawayKey);
}
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("giveawayId", giveaway.getGiveawayId().getId().toString());
parameters.put("serverId", giveaway.getGiveawayId().getServerId().toString());
@@ -264,7 +328,7 @@ public class GiveawayServiceBean implements GiveawayService {
.builder()
.parameters(parameters)
.build();
log.info("Scheduling giveaway reminder for giveaway {} originating from message {} in server {}.", giveaway.getGiveawayId().getId(), messageId, creatorMember.getGuild().getIdLong());
log.info("Scheduling giveaway reminder for giveaway {} originating from message {} in server {}.", giveaway.getGiveawayId().getId(), messageId, giveawayCreationRequest.getServerId());
String triggerKey = schedulerService.executeJobWithParametersOnce("giveawayEvaluationJob", "giveaway", jobParameters, Date.from(giveaway.getTargetDate()));
giveaway.setReminderTriggerKey(triggerKey);
JoinGiveawayPayload joinPayload = JoinGiveawayPayload

View File

@@ -0,0 +1,83 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.CounterService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.giveaway.exception.GiveawayKeyNotFoundException;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayKey;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayKeyId;
import dev.sheldan.abstracto.giveaway.repository.GiveawayKeyRepository;
import java.util.List;
import java.util.Optional;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class GiveawayKeyManagementServiceBean implements GiveawayKeyManagementService {
@Autowired
private GiveawayKeyRepository giveawayKeyRepository;
@Autowired
private CounterService counterService;
@Autowired
private UserInServerManagementService userInServerManagementService;
public static final String GIVEAWAY_KEYS_COUNTER = "giveaway_keys";
@Override
public GiveawayKey createGiveawayKey(Member creator, Member benefactor, String key, String description, String name) {
Long counterValue = counterService.getNextCounterValue(creator.getGuild().getIdLong(), GIVEAWAY_KEYS_COUNTER);
GiveawayKeyId id = new GiveawayKeyId(counterValue, creator.getGuild().getIdLong());
AUserInAServer creatorUser = userInServerManagementService.loadOrCreateUser(creator);
AUserInAServer benefactorUser;
if(benefactor != null) {
benefactorUser = userInServerManagementService.loadOrCreateUser(benefactor);
} else {
benefactorUser = null;
}
GiveawayKey giveawayKey = GiveawayKey
.builder()
.id(id)
.creator(creatorUser)
.used(false)
.server(creatorUser.getServerReference())
.key(key)
.description(description)
.benefactor(benefactorUser)
.name(name)
.build();
return giveawayKeyRepository.save(giveawayKey);
}
@Override
public void deleteById(Long id, Long serverId) {
GiveawayKey key = giveawayKeyRepository.findById(new GiveawayKeyId(id, serverId)).orElseThrow(GiveawayKeyNotFoundException::new);
key.getGiveaway().setGiveawayKey(null);
giveawayKeyRepository.delete(key);
}
@Override
public Optional<GiveawayKey> getById(Long id, Long serverId) {
return giveawayKeyRepository.findById(new GiveawayKeyId(id, serverId));
}
@Override
public GiveawayKey saveGiveawayKey(GiveawayKey giveawayKey) {
return giveawayKeyRepository.save(giveawayKey);
}
@Override
public List<GiveawayKey> getGiveawayKeys(Long serverId, Boolean showAll) {
if(showAll) {
return giveawayKeyRepository.findGiveawayKeysByServer_IdOrderById(serverId);
} else {
return giveawayKeyRepository.findGiveawayKeysByUsedAndServer_IdOrderById(false, serverId);
}
}
}

View File

@@ -1,4 +1,15 @@
abstracto.featureFlags.giveaway.featureName=giveaway
abstracto.featureFlags.giveaway.enabled=false
abstracto.postTargets.giveaways.name=giveaways
abstracto.postTargets.giveaways.name=giveaways
abstracto.featureModes.keyGiveaways.featureName=giveaway
abstracto.featureModes.keyGiveaways.mode=keyGiveaways
abstracto.featureModes.keyGiveaways.enabled=false
abstracto.featureModes.autoNotifyGiveawayKeyWinners.featureName=giveaway
abstracto.featureModes.autoNotifyGiveawayKeyWinners.mode=autoNotifyGiveawayKeyWinners
abstracto.featureModes.autoNotifyGiveawayKeyWinners.enabled=false
abstracto.systemConfigs.keyGiveawaysDuration.name=keyGiveawaysDuration
abstracto.systemConfigs.keyGiveawaysDuration.stringValue=24h

View File

@@ -0,0 +1,7 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,30 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.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="giveawayKey-commands">
<insert tableName="command">
<column name="name" value="addGiveawayKey"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="removeGiveawayKey"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="showGiveawayKeys"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="createKeyGiveaway"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${giveawayFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,69 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<changeSet author="Sheldan" id="giveaway_key-table">
<createTable tableName="giveaway_key">
<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="winner_user_id" type="INTEGER">
<constraints nullable="true"/>
</column>
<column name="key" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="used" type="BOOLEAN">
<constraints nullable="false"/>
</column>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="giveaway_id" type="BIGINT">
<constraints nullable="true" />
</column>
<column name="giveaway_server_id" type="BIGINT">
<constraints nullable="true" />
</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_key" columnNames="id, server_id"/>
<addForeignKeyConstraint baseColumnNames="creator_user_id" baseTableName="giveaway_key" constraintName="fk_giveaway_key_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_key" constraintName="fk_giveaway_key_benefactor" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="user_in_server_id"
referencedTableName="user_in_server" validate="true"/>
<addForeignKeyConstraint baseColumnNames="winner_user_id" baseTableName="giveaway_key" constraintName="fk_giveaway_key_winner" 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,giveaway_server_id" baseTableName="giveaway_key" constraintName="fk_giveaway_key_giveaway" deferrable="false"
initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id,server_id"
referencedTableName="giveaway" validate="true"/>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="giveaway_key" constraintName="fk_giveaway_key_server" deferrable="false" initiallyDeferred="false"
onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS giveaway_key_update_trigger ON giveaway_key;
CREATE TRIGGER giveaway_update_trigger BEFORE UPDATE ON giveaway_key FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS giveaway_key_insert_trigger ON giveaway_key;
CREATE TRIGGER giveaway_insert_trigger BEFORE INSERT ON giveaway_key FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="giveaway_key.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -3,4 +3,5 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
<include file="1.5.13/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.55/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -2,6 +2,7 @@ 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.FeatureMode;
import dev.sheldan.abstracto.core.config.PostTargetEnum;
import org.springframework.stereotype.Component;
@@ -11,6 +12,8 @@ import java.util.List;
@Component
public class GiveawayFeatureConfig implements FeatureConfig {
public static final String KEY_GIVEAWAYS_DURATION = "keyGiveawaysDuration";
@Override
public FeatureDefinition getFeature() {
return GiveawayFeatureDefinition.GIVEAWAY;
@@ -20,4 +23,14 @@ public class GiveawayFeatureConfig implements FeatureConfig {
public List<PostTargetEnum> getRequiredPostTargets() {
return Arrays.asList(GiveawayPostTarget.GIVEAWAYS);
}
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(GiveawayMode.KEY_GIVEAWAYS, GiveawayMode.AUTO_NOTIFY_GIVEAWAY_KEY_WINNERS);
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(KEY_GIVEAWAYS_DURATION);
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.giveaway.config;
import dev.sheldan.abstracto.core.config.FeatureMode;
import lombok.Getter;
@Getter
public enum GiveawayMode implements FeatureMode {
KEY_GIVEAWAYS("keyGiveaways"),
AUTO_NOTIFY_GIVEAWAY_KEY_WINNERS("autoNotifyGiveawayKeyWinners");
private final String key;
GiveawayMode(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.giveaway.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
public class GiveawayKeyNotFoundException extends AbstractoTemplatableException {
public GiveawayKeyNotFoundException() {
super("Giveaway key not found.");
}
@Override
public String getTemplateName() {
return "giveaway_key_not_found_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.giveaway.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
public class GiveawayNotPossibleException extends AbstractoTemplatableException {
public GiveawayNotPossibleException() {
super("Giveaway not possible.");
}
@Override
public String getTemplateName() {
return "giveaway_not_possible_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -3,7 +3,6 @@ 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;
@@ -12,8 +11,10 @@ import java.time.Duration;
@Getter
public class GiveawayCreationRequest {
private Member creator;
private Member benefactor;
private Long creatorId;
private Long serverId;
private Long benefactorId;
private Long giveawayKeyId;
private String title;
private String description;
private Duration duration;

View File

@@ -65,6 +65,9 @@ public class Giveaway {
@Column(name = "message_id", nullable = false)
private Long messageId;
@OneToOne(mappedBy = "giveaway")
private GiveawayKey giveawayKey;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;

View File

@@ -0,0 +1,75 @@
package dev.sheldan.abstracto.giveaway.model.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.giveaway.model.database.embed.GiveawayKeyId;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "giveaway_key")
@Getter
@Setter
@EqualsAndHashCode
public class GiveawayKey {
@Id
@EmbeddedId
private GiveawayKeyId id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumns(
{
@JoinColumn(name = "giveaway_id", referencedColumnName = "id"),
@JoinColumn(name = "giveaway_server_id", referencedColumnName = "server_id")
})
private Giveaway giveaway;
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@MapsId("serverId")
@JoinColumn(name = "server_id", referencedColumnName = "id", nullable = false)
private AServer server;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_user_id", nullable = false)
private AUserInAServer creator;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "winner_user_id")
private AUserInAServer winner;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "benefactor_user_id")
private AUserInAServer benefactor;
@Column(name = "key", nullable = false)
private String key;
@Column(name = "name")
private String name;
@Column(name = "used")
private Boolean used;
@Column(name = "description")
private String description;
}

View File

@@ -0,0 +1,27 @@
package dev.sheldan.abstracto.giveaway.model.database.embed;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Embeddable
@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class GiveawayKeyId implements Serializable {
@Column(name = "id")
private Long keyId;
@Column(name = "server_id")
private Long serverId;
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.giveaway.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class GiveawayKeyDisplayModel {
private String key;
private String name;
private Long id;
private String description;
private Boolean used;
private MemberDisplay creator;
private MemberDisplay benefactor;
private MemberDisplay winner;
}

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.giveaway.model.template;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class GiveawayKeysDisplayModel {
private List<GiveawayKeyDisplayModel> keys;
}

View File

@@ -0,0 +1,31 @@
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;
@Builder
@Getter
@Setter
public class GiveawayWinnerNotificationMessageModel {
private String title;
private String description;
private String key;
private Long giveawayId;
private MemberDisplay creator;
private MemberDisplay benefactor;
public static GiveawayWinnerNotificationMessageModel fromGiveaway(Giveaway giveaway, String key) {
return GiveawayWinnerNotificationMessageModel
.builder()
.title(giveaway.getTitle())
.description(giveaway.getDescription())
.key(key)
.benefactor(giveaway.getBenefactor() != null ? MemberDisplay.fromAUserInAServer(giveaway.getBenefactor()) : null)
.creator(MemberDisplay.fromAUserInAServer(giveaway.getCreator()))
.giveawayId(giveaway.getGiveawayId().getId())
.build();
}
}

View File

@@ -8,7 +8,7 @@ import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.concurrent.CompletableFuture;
public interface GiveawayService {
CompletableFuture<Void> createGiveaway(GiveawayCreationRequest giveawayCreationRequest);
CompletableFuture<Long> 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,14 @@
package dev.sheldan.abstracto.giveaway.service.management;
import dev.sheldan.abstracto.giveaway.model.database.GiveawayKey;
import java.util.List;
import java.util.Optional;
import net.dv8tion.jda.api.entities.Member;
public interface GiveawayKeyManagementService {
GiveawayKey createGiveawayKey(Member creator, Member benefactor, String key, String description, String name);
void deleteById(Long id, Long serverId);
Optional<GiveawayKey> getById(Long id, Long serverId);
GiveawayKey saveGiveawayKey(GiveawayKey giveawayKey);
List<GiveawayKey> getGiveawayKeys(Long serverId, Boolean ignoreUsedFlag);
}

View File

@@ -15,10 +15,10 @@ import dev.sheldan.abstracto.moderation.model.interaction.ModerationActionMutePa
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionMuteModalModel;
import dev.sheldan.abstracto.moderation.model.template.listener.ModerationActionPayloadModel;
import dev.sheldan.abstracto.moderation.service.ModerationActionServiceBean;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j

View File

@@ -17,6 +17,7 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.core.utils.SnowflakeUtils;
import dev.sheldan.abstracto.moderation.service.BanService;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig;
import dev.sheldan.abstracto.modmail.config.ModMailFeatureDefinition;
@@ -28,6 +29,7 @@ import dev.sheldan.abstracto.modmail.exception.ModMailThreadNotFoundException;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.dto.ServiceChoicesPayload;
import dev.sheldan.abstracto.modmail.model.database.*;
import dev.sheldan.abstracto.modmail.model.listener.ModmailThreadCreatedSendMessageModel;
import dev.sheldan.abstracto.modmail.model.template.*;
import dev.sheldan.abstracto.modmail.service.management.ModMailMessageManagementService;
import dev.sheldan.abstracto.modmail.service.management.ModMailRoleManagementService;
@@ -46,6 +48,7 @@ import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
import net.dv8tion.jda.api.interactions.InteractionHook;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -56,6 +59,7 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.transaction.event.TransactionalEventListener;
import static dev.sheldan.abstracto.modmail.config.ModMailFeatureConfig.MOD_MAIL_CLOSING_TEXT_SYSTEM_CONFIG_KEY;
@@ -159,6 +163,9 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Autowired
private InteractionService interactionService;
@Autowired
private ApplicationEventPublisher eventPublisher;
public static final String MODMAIL_THREAD_METRIC = "modmail.threads";
public static final String MODMAIL_MESSAGE_METRIC = "modmail.messges";
public static final String ACTION = "action";
@@ -189,8 +196,8 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
public static final String MODMAIL_INITIAL_ORIGIN = "modmailInitial";
@Override
public CompletableFuture<MessageChannel> createModMailThreadForUser(User user, Guild guild, Message initialMessage, boolean userInitiated, List<UndoActionInstance> undoActions, boolean appeal) {
public CompletableFuture<MessageChannel> createModMailThreadForUser(User user, Guild guild, Message initialMessage, boolean userInitiated, List<UndoActionInstance> undoActions,
boolean appeal, ModmailThreadCreatedSendMessageModel createdSendMessageModel) {
Long serverId = guild.getIdLong();
AServer server = serverManagementService.loadServer(serverId);
metricService.incrementCounter(MODMAIL_THREAD_CREATED_COUNTER);
@@ -220,7 +227,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
.thenCompose(unused -> channelService.createThreadWithStarterMessage(textChannel, channelName, notificationMessage.get(0).join().getIdLong()))
.thenCompose(threadChannel -> {
undoActions.add(UndoActionInstance.getChannelDeleteAction(serverId, threadChannel.getIdLong()));
return self.performModMailThreadSetup(user, initialMessage, threadChannel, userInitiated, undoActions, appeal)
return self.performModMailThreadSetup(user, initialMessage, threadChannel, userInitiated, undoActions, appeal, createdSendMessageModel)
.thenCompose(unused -> CompletableFuture.completedFuture(threadChannel));
});
} else {
@@ -229,12 +236,18 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
CompletableFuture<TextChannel> textChannelFuture = channelService.createTextChannel(channelName, server, categoryId);
return textChannelFuture.thenCompose(channel -> {
undoActions.add(UndoActionInstance.getChannelDeleteAction(serverId, channel.getIdLong()));
return self.performModMailThreadSetup(user, initialMessage, channel, userInitiated, undoActions, appeal)
return self.performModMailThreadSetup(user, initialMessage, channel, userInitiated, undoActions, appeal, createdSendMessageModel)
.thenCompose(unused -> CompletableFuture.completedFuture(channel));
});
}
}
@Override
public CompletableFuture<MessageChannel> createModMailThreadForUser(User user, Guild guild, Message initialMessage, boolean userInitiated,
List<UndoActionInstance> undoActions, boolean appeal) {
return createModMailThreadForUser(user, guild, initialMessage, userInitiated, undoActions, appeal, null);
}
@Transactional
@Override
public CompletableFuture<Void> sendContactNotification(User user, MessageChannel messageChannel, MessageChannel feedBackChannel) {
@@ -265,10 +278,12 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @param userInitiated Whether the thread was initiated by a member
* @param undoActions The list of actions to undo, in case an exception occurs
* @param appeal Whether the modmail thread is for the purpose of an appeal
* @param createdSendMessageModel The information which message should be sent after the modmail thread is completely created
* @return A {@link CompletableFuture future} which completes when the setup is done
*/
@Transactional
public CompletableFuture<Void> performModMailThreadSetup(User user, Message initialMessage, GuildMessageChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions, boolean appeal) {
public CompletableFuture<Void> performModMailThreadSetup(User user, Message initialMessage, GuildMessageChannel channel, boolean userInitiated, List<UndoActionInstance> undoActions,
boolean appeal, ModmailThreadCreatedSendMessageModel createdSendMessageModel) {
log.info("Performing modmail thread setup for channel {} for user {} in server {}. It was initiated by a user: {}.", channel.getIdLong(), user.getId(), channel.getGuild().getId(), userInitiated);
CompletableFuture<Void> headerFuture = sendModMailHeader(channel, user);
CompletableFuture<Message> userReplyMessage;
@@ -287,18 +302,22 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
return CompletableFuture.allOf(headerFuture, notificationFuture, userReplyMessage).thenAccept(aVoid -> {
undoActions.clear();
self.setupModMailThreadInDB(initialMessage, channel, user, userReplyMessage.join(), appeal);
self.setupModMailThreadInDB(initialMessage, channel, user, userReplyMessage.join(), appeal, createdSendMessageModel);
});
}
@Transactional
public void setupModMailThreadInDB(Message initialMessage, GuildMessageChannel channel, User user, Message sendMessage, boolean appeal) {
public void setupModMailThreadInDB(Message initialMessage, GuildMessageChannel channel, User user, Message sendMessage, boolean appeal,
ModmailThreadCreatedSendMessageModel createdSendMessageModel) {
log.info("Persisting info about modmail thread {} in database.", channel.getIdLong());
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(channel.getGuild().getIdLong(), user.getIdLong());
ModMailThread thread = createThreadObject(channel, aUserInAServer, appeal);
if(initialMessage != null) {
log.debug("Adding initial message {} to modmail thread in channel {}.", initialMessage.getId(), channel.getId());
modMailMessageManagementService.addMessageToThread(thread, null, sendMessage, initialMessage, aUserInAServer, false, false);
modMailMessageManagementService.addMessageToThread(thread, null, sendMessage, initialMessage.getIdLong(), aUserInAServer, false, false);
}
if(createdSendMessageModel != null) {
eventPublisher.publishEvent(createdSendMessageModel);
}
}
@@ -527,6 +546,59 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
}
}
@Override
public CompletableFuture<Void> sendMessageToUser(AUserInAServer aUserInAServer, MessageToSend messageToSendToUser, User user) {
AServer aServer = aUserInAServer.getServerReference();
Long serverId = aServer.getId();
Long userId = user.getIdLong();
Guild guild = guildService.getGuildById(serverId);
if(modMailThreadManagementService.hasOpenModMailThreadForUser(aUserInAServer)) {
return sendMessageToExistingModmailThread(aUserInAServer, messageToSendToUser, user);
} else {
List<UndoActionInstance> undoActionInstances = new ArrayList<>();
ModmailThreadCreatedSendMessageModel createdSendMessageModel = ModmailThreadCreatedSendMessageModel
.builder()
.messageToSend(messageToSendToUser)
.userId(userId)
.serverId(serverId)
.build();
return createModMailThreadForUser(user, guild, null, false, undoActionInstances, false, createdSendMessageModel).thenAccept(messageChannel -> {
}).exceptionally(throwable -> {
log.error("Failed to setup modmail thread correctly", throwable);
undoActionService.performActions(undoActionInstances);
throw new AbstractoRunTimeException(throwable);
});
}
}
private CompletableFuture<Void> sendMessageToExistingModmailThread(AUserInAServer aUserInAServer, MessageToSend messageToSendToUser, User user) {
ModMailThread modmailThread = modMailThreadManagementService.getOpenModMailThreadForUser(aUserInAServer);
CompletableFuture<Message> future = messageService.sendMessageToSendToUser(user, messageToSendToUser);
CompletableFuture<Message> sameThreadMessageFuture = channelService.sendMessageEmbedToSendToAChannel(messageToSendToUser, modmailThread.getChannel()).get(0);
Long modmailThreadId = modmailThread.getId();
Guild guild = guildService.getGuildById(aUserInAServer.getServerReference().getId());
return CompletableFuture.allOf(future, sameThreadMessageFuture).thenAccept(avoid ->
self.saveSendMessagesAndUpdateState(modmailThreadId, false, future.join(), guild.getSelfMember(), SnowflakeUtils.createSnowFlake(), sameThreadMessageFuture.join())
);
}
@TransactionalEventListener
public void sendMessageAfterModmailThreadCreated(ModmailThreadCreatedSendMessageModel model) {
log.info("Sending message to {} in server {} after modmail thread was created.", model.getUserId(), model.getServerId());
userService.retrieveUserForId(model.getUserId())
.thenCompose(user -> self.loadUserAndSendtoModmail(model, user))
.exceptionally(throwable -> {
log.warn("Failed to load user to send modmail thread to.");
return null;
});
}
@Transactional
public CompletableFuture<Void> loadUserAndSendtoModmail(ModmailThreadCreatedSendMessageModel model, User user) {
AUserInAServer aUserInAServer = userInServerManagementService.onlyLoadUser(model.getServerId(), model.getUserId());
return sendMessageToExistingModmailThread(aUserInAServer, model.getMessageToSend(), user);
}
/**
* This message takes a received {@link Message} from a user, renders it to a new message to send and sends it to
* the appropriate {@link ModMailThread} channel, the returned promise only returns if the message was dealt with on the user
@@ -622,7 +694,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
log.debug("Adding created message {} based on messeage {} sent from user to modmail thread {} and setting status to {}.", messageInModMailThread.getId(), messageFromUser.getId(), modMailThread.getId(), ModMailThreadState.USER_REPLIED);
modMailMessageManagementService.addMessageToThread(modMailThread, null, messageInModMailThread, messageFromUser, modMailThread.getUser(), false, false);
modMailMessageManagementService.addMessageToThread(modMailThread, null, messageInModMailThread, messageFromUser.getIdLong(), modMailThread.getUser(), false, false);
// update the state of the thread
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.USER_REPLIED);
} else {
@@ -678,7 +750,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
sameThreadMessageFuture = CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(future, sameThreadMessageFuture).thenAccept(avoid ->
self.saveSendMessagesAndUpdateState(modmailThreadId, anonymous, future.join(), replyCommandMessage, sameThreadMessageFuture.join())
self.saveSendMessagesAndUpdateState(modmailThreadId, anonymous, future.join(), replyCommandMessage.getMember(), replyCommandMessage.getIdLong(), sameThreadMessageFuture.join())
);
}
@@ -1009,17 +1081,18 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
* @param anonymous Whether or not the messages were send anonymous
* @param createdMessageInDM The {@link Message message} which was sent to the private channel with the {@link User user}
* @param modMailThreadMessage The {@link Message message} which was sent in the channel representing the {@link ModMailThread thread}. Might be null.
* @param replyCommandMessage The {@link Message message} which contained the command used to reply to the user
* @param author The {@link Member} which caused the message to be sent
* @param messageId The ID of the message that was sent
* @throws ModMailThreadNotFoundException in case the {@link ModMailThread} is not found by the ID
*/
@Transactional
public void saveSendMessagesAndUpdateState(Long modMailThreadId, Boolean anonymous, Message createdMessageInDM, Message replyCommandMessage, Message modMailThreadMessage) {
public void saveSendMessagesAndUpdateState(Long modMailThreadId, Boolean anonymous, Message createdMessageInDM, Member author, Long messageId, Message modMailThreadMessage) {
Optional<ModMailThread> modMailThreadOpt = modMailThreadManagementService.getByIdOptional(modMailThreadId);
AUserInAServer moderator = userInServerManagementService.loadOrCreateUser(replyCommandMessage.getMember());
AUserInAServer moderator = userInServerManagementService.loadOrCreateUser(author);
if(modMailThreadOpt.isPresent()) {
ModMailThread modMailThread = modMailThreadOpt.get();
log.debug("Adding (anonymous: {}) message {} of moderator to modmail thread {} and setting state to {}.", anonymous, createdMessageInDM.getId(), modMailThreadId, ModMailThreadState.MOD_REPLIED);
modMailMessageManagementService.addMessageToThread(modMailThread, createdMessageInDM, modMailThreadMessage, replyCommandMessage, moderator, anonymous, true);
modMailMessageManagementService.addMessageToThread(modMailThread, createdMessageInDM, modMailThreadMessage, messageId, moderator, anonymous, true);
modMailThreadManagementService.setModMailThreadState(modMailThread, ModMailThreadState.MOD_REPLIED);
} else {
throw new ModMailThreadNotFoundException(modMailThreadId);

View File

@@ -20,13 +20,13 @@ public class ModMailMessageManagementServiceBean implements ModMailMessageManage
private ModMailMessageRepository modMailMessageRepository;
@Override
public ModMailMessage addMessageToThread(ModMailThread modMailThread, Message createdMessageInDM, Message createdMessageInChannel, Message userPostedMessage, AUserInAServer author, Boolean anonymous, Boolean dmChannel) {
public ModMailMessage addMessageToThread(ModMailThread modMailThread, Message createdMessageInDM, Message createdMessageInChannel, Long messageId, AUserInAServer author, Boolean anonymous, Boolean dmChannel) {
Long dmId = createdMessageInDM != null ? createdMessageInDM.getIdLong() : null;
Long channelMessageId = createdMessageInChannel != null ? createdMessageInChannel.getIdLong() : null;
ModMailMessage modMailMessage = ModMailMessage
.builder()
.author(author)
.messageId(userPostedMessage.getIdLong())
.messageId(messageId)
.createdMessageInDM(dmId)
.server(modMailThread.getServer())
.createdMessageInChannel(channelMessageId)
@@ -35,7 +35,7 @@ public class ModMailMessageManagementServiceBean implements ModMailMessageManage
.anonymous(anonymous)
.build();
log.info("Storing created message in DM {} with created message in channel {} caused by message {} to modmail thread {} of user {} in server {}.",
dmId, channelMessageId, userPostedMessage.getId(), modMailThread.getId(), author.getUserReference().getId(), author.getServerReference().getId());
dmId, channelMessageId, messageId, modMailThread.getId(), author.getUserReference().getId(), author.getServerReference().getId());
return modMailMessageRepository.save(modMailMessage);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.modmail.model.listener;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class ModmailThreadCreatedSendMessageModel {
private MessageToSend messageToSend;
private Long serverId;
private Long userId;
}

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.models.UndoActionInstance;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.modmail.model.ClosingContext;
import dev.sheldan.abstracto.modmail.model.database.ModMailThread;
import net.dv8tion.jda.api.entities.*;
@@ -62,6 +63,7 @@ public interface ModMailThreadService {
* @return A {@link CompletableFuture future} which completes when the message has been relayed to the channel
*/
CompletableFuture<Message> relayMessageToModMailThread(ModMailThread modMailThread, Message messageFromUser, List<UndoActionInstance> undoActions);
CompletableFuture<Void> sendMessageToUser(AUserInAServer aUserInAServer, MessageToSend messageToSend, User user);
/**
* Forwards a message send by a moderator to the direct message channel opened with the user. If the message is

View File

@@ -17,13 +17,13 @@ public interface ModMailMessageManagementService {
* @param modMailThread The {@link ModMailThread} the message should be attached to
* @param createdMessageInDM The {@link Message} which should be attached to the {@link ModMailThread} and was posted to the DM channel (might be null)
* @param createdMessageInChannel The {@link Message} which should be attached to the {@link ModMailThread} and was posted to the modmail thread (might be null)
* @param userPostedMessage The {@link Message} which caused this message to be created, the command or the message by the user
* @param messageId The ID of the {@link Message} which caused this message to be created, the command or the message by the user
* @param author The {@link AUserInAServer} who authored the {@link Message} originally
* @param anonymous Whether or not the message was sent anonymous (only possible by staff members)
* @param dmChannel Whether or not the message originated from the user, and therefore in an direct message channel
* @return The created {@link ModMailMessage message} instance
*/
ModMailMessage addMessageToThread(ModMailThread modMailThread, Message createdMessageInDM, Message createdMessageInChannel, Message userPostedMessage, AUserInAServer author, Boolean anonymous, Boolean dmChannel);
ModMailMessage addMessageToThread(ModMailThread modMailThread, Message createdMessageInDM, Message createdMessageInChannel, Long messageId, AUserInAServer author, Boolean anonymous, Boolean dmChannel);
/**
* Retrieves all messages which were sent in a {@link ModMailThread}

View File

@@ -66,6 +66,9 @@ public class MessageUpdatedListenerBean extends ListenerAdapter {
@Override
@Transactional
public void onMessageUpdate(@Nonnull MessageUpdateEvent event) {
if(!event.isFromGuild()) {
return;
}
metricService.incrementCounter(MESSAGE_UPDATED_COUNTER);
Message message = event.getMessage();
messageCache.getMessageFromCache(message.getGuild().getIdLong(), message.getChannel().getIdLong(), event.getMessageIdLong()).thenAccept(cachedMessage -> {