[AB-25] refactoring experience collection to work instantly instead of delayed job

adding level up notification for experience
This commit is contained in:
Sheldan
2022-11-20 15:14:43 +01:00
parent d315113395
commit 5c7b018b2a
41 changed files with 547 additions and 1736 deletions

View File

@@ -10,8 +10,6 @@ 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.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import lombok.extern.slf4j.Slf4j;
@@ -34,12 +32,6 @@ public class SetExpRole extends AbstractConditionableCommand {
@Autowired
private ExperienceRoleService experienceRoleService;
@Autowired
private RoleService roleService;
@Autowired
private RoleManagementService roleManagementService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Integer level = (Integer) commandContext.getParameters().getParameters().get(0);
@@ -48,7 +40,7 @@ public class SetExpRole extends AbstractConditionableCommand {
throw new EntityGuildMismatchException();
}
log.info("Setting role {} to be used for level {} on server {}", role.getId(), level, role.getGuild().getId());
return experienceRoleService.setRoleToLevel(role, level, commandContext.getChannel().getIdLong())
return experienceRoleService.setRoleToLevel(role, level, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@@ -56,8 +48,21 @@ public class SetExpRole extends AbstractConditionableCommand {
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
List<ParameterValidator> levelValidators = Arrays.asList(MinIntegerValueValidator.min(0L));
parameters.add(Parameter.builder().name("level").validators(levelValidators).templated(true).type(Integer.class).build());
parameters.add(Parameter.builder().name("role").templated(true).type(Role.class).build());
Parameter level = Parameter
.builder()
.name("level")
.validators(levelValidators)
.templated(true)
.type(Integer.class)
.build();
parameters.add(level);
Parameter role = Parameter
.builder()
.name("role")
.templated(true)
.type(Role.class)
.build();
parameters.add(role);
HelpInfo helpInfo = HelpInfo.builder().templated(true).hasExample(true).build();
return CommandConfiguration.builder()
.name("setExpRole")

View File

@@ -39,7 +39,7 @@ public class SyncRoles extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AServer server = serverManagementService.loadServer(commandContext.getGuild());
log.info("Synchronizing roles on server {}", server.getId());
return userExperienceService.syncUserRolesWithFeedback(server, commandContext.getChannel().getIdLong())
return userExperienceService.syncUserRolesWithFeedback(server, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromIgnored());
}

View File

@@ -52,7 +52,7 @@ public class UnSetExpRole extends AbstractConditionableCommand {
if(!experienceRole.isPresent()) {
throw new ExperienceRoleNotFoundException();
}
return experienceRoleService.unsetRoles(actualRole, commandContext.getChannel().getIdLong())
return experienceRoleService.unsetRoles(actualRole, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -1,6 +1,5 @@
package dev.sheldan.abstracto.experience.job;
import dev.sheldan.abstracto.experience.model.ServerExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.RunTimeExperienceService;
import lombok.extern.slf4j.Slf4j;
@@ -12,10 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* This {@link QuartzJobBean job} is executed regularly and calls the the {@link AUserExperienceService service}
@@ -26,10 +21,7 @@ import java.util.Map;
@DisallowConcurrentExecution
@Component
@PersistJobDataAfterExecution
public class ExperiencePersistingJob extends QuartzJobBean {
@Autowired
private AUserExperienceService userExperienceService;
public class ExperienceCleanupJob extends QuartzJobBean {
@Autowired
private RunTimeExperienceService runTimeExperienceService;
@@ -37,19 +29,9 @@ public class ExperiencePersistingJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
runTimeExperienceService.takeLock();
log.info("Cleaning up experience runtime storage.");
try {
Map<Long, List<ServerExperience>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
log.info("Running experience persisting job.");
Long pastMinute = (Instant.now().getEpochSecond() / 60) - 1;
if(runtimeExperience.containsKey(pastMinute)) {
List<ServerExperience> foundServers = runtimeExperience.get(pastMinute);
log.info("Found experience from {} servers to persist.", foundServers.size());
userExperienceService.handleExperienceGain(foundServers).thenAccept(aVoid -> {
runTimeExperienceService.takeLock();
runTimeExperienceService.getRuntimeExperience().remove(pastMinute);
runTimeExperienceService.releaseLock();
});
}
runTimeExperienceService.cleanupRunTimeStorage();
} finally {
runTimeExperienceService.releaseLock();
}

View File

@@ -4,9 +4,7 @@ import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncMessageReceivedListener;
import dev.sheldan.abstracto.core.listener.sync.jda.MessageReceivedListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import lombok.extern.slf4j.Slf4j;
@@ -25,9 +23,6 @@ public class ExperienceTrackerListener implements AsyncMessageReceivedListener {
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
@@ -35,8 +30,7 @@ public class ExperienceTrackerListener implements AsyncMessageReceivedListener {
return DefaultListenerResult.IGNORED;
}
if(userExperienceService.experienceGainEnabledInChannel(message.getChannel())) {
AUserInAServer cause = userInServerManagementService.loadOrCreateUser(model.getServerId(), model.getMessage().getAuthor().getIdLong());
userExperienceService.addExperience(cause);
userExperienceService.addExperience(message.getMember(), model.getMessage());
return DefaultListenerResult.PROCESSED;
} else {
return DefaultListenerResult.IGNORED;

View File

@@ -42,7 +42,7 @@ public class JoiningUserRoleListener implements AsyncJoinListener {
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
if(userExperienceOptional.isPresent()) {
log.info("User {} joined {} with previous experience. Setting up experience role again (if necessary).", model.getJoiningUser().getUserId(), model.getServerId());
userExperienceService.syncForSingleUser(userExperienceOptional.get()).thenAccept(result ->
userExperienceService.syncForSingleUser(userExperienceOptional.get(), model.getMember()).thenAccept(result ->
log.info("Finished re-assigning experience for re-joining user {} in server {}.", model.getJoiningUser().getUserId(), model.getServerId())
);
} else {

View File

@@ -21,6 +21,7 @@ public interface ExperienceRoleRepository extends JpaRepository<AExperienceRole,
* @return The {@link AExperienceRole experienceRole} found or null if the query did not return any results
*/
Optional<AExperienceRole> findByRole(ARole role);
List<AExperienceRole> findByRole_IdIn(List<Long> role);
/**
* Finds a list of {@link AExperienceRole experienceRoles} (if there are multiple ones, because of misconfiguration) of the given

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@@ -52,4 +53,8 @@ public interface UserExperienceRepository extends JpaRepository<AUserExperience
"WHERE rank.id = :userInServerId", nativeQuery = true)
LeaderBoardEntryResult getRankOfUserInServer(@Param("userInServerId") Long id, @Param("serverId") Long serverId);
@Modifying(clearAutomatically = true)
@Query("update AUserExperience u set u.currentExperienceRole = null where u.currentExperienceRole.id = :roleId")
void removeExperienceRoleFromUsers(@Param("roleId") Long experienceRoleId);
}

View File

@@ -1,36 +1,49 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.property.SystemConfigProperty;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.*;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.exception.NoExperienceTrackedException;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.LeaderBoard;
import dev.sheldan.abstracto.experience.model.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.model.RoleCalculationResult;
import dev.sheldan.abstracto.experience.model.database.*;
import dev.sheldan.abstracto.experience.model.template.LevelUpNotificationModel;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.DisabledExpRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
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.MessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig.EXP_COOLDOWN_SECONDS_KEY;
@Component
@Slf4j
public class AUserExperienceServiceBean implements AUserExperienceService {
@@ -81,38 +94,49 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
private ChannelGroupService channelGroupService;
@Autowired
private DefaultConfigManagementService defaultConfigManagementService;
private SecureRandom secureRandom;
@Autowired
private ChannelService channelService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private AUserExperienceServiceBean self;
@Autowired
@Qualifier("experienceUpdateExecutor")
private TaskExecutor experienceUpdateExecutor;
@Override
public void addExperience(AUserInAServer userInAServer) {
public void addExperience(Member member, Message message) {
runTimeExperienceService.takeLock();
try {
Long minute = Instant.now().getEpochSecond() / 60;
Map<Long, List<ServerExperience>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
Long serverId = userInAServer.getServerReference().getId();
Long userInServerId = userInAServer.getUserInServerId();
if(runtimeExperience.containsKey(minute)) {
log.debug("Minute {} already tracked, adding user {} in server {}.",
minute, userInAServer.getUserReference().getId(), serverId);
List<ServerExperience> existing = runtimeExperience.get(minute);
for (ServerExperience server : existing) {
if (server.getServerId().equals(serverId) && server.getUserInServerIds().stream().noneMatch(userInServerId::equals)) {
server.getUserInServerIds().add(userInServerId);
break;
Map<Long, Map<Long, Instant>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
Long serverId = member.getGuild().getIdLong();
Long userId = member.getIdLong();
boolean receivesNewExperience = false;
if(!runtimeExperience.containsKey(serverId)) {
runtimeExperience.put(serverId, new HashMap<>());
receivesNewExperience = true;
} else {
Map<Long, Instant> serverExperience = runtimeExperience.get(serverId);
if(!serverExperience.containsKey(userId)) {
receivesNewExperience = true;
} else {
Instant latestExperience = serverExperience.get(userId);
if(latestExperience.isBefore(Instant.now())) {
receivesNewExperience = true;
}
}
} else {
log.debug("Minute {} did not exist yet. Creating new entry for user {} in server {}.", minute, userInAServer.getUserReference().getId(), serverId);
ServerExperience serverExperience = ServerExperience
.builder()
.serverId(serverId)
.build();
serverExperience.getUserInServerIds().add(userInServerId);
runtimeExperience.put(minute, new ArrayList<>(Arrays.asList(serverExperience)));
}
if(receivesNewExperience) {
Map<Long, Instant> serverExperience = runtimeExperience.get(serverId);
// we store when the user is eligible for experience _again_
Long maxSeconds = configService.getLongValueOrConfigDefault(EXP_COOLDOWN_SECONDS_KEY, serverId);
serverExperience.put(userId, Instant.now().plus(maxSeconds, ChronoUnit.SECONDS));
CompletableFuture.runAsync(() -> self.addExperienceToMember(member, message), experienceUpdateExecutor);
}
} finally {
runTimeExperienceService.releaseLock();
@@ -148,317 +172,199 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return false;
}
@Transactional
@Override
public CompletableFuture<Void> handleExperienceGain(List<ServerExperience> servers) {
List<ExperienceGainResult> resultFutures = new ArrayList<>();
List<CompletableFuture<RoleCalculationResult>> futures = new ArrayList<>();
CompletableFuture<Void> experienceFuture = new CompletableFuture<>();
// TODO what if there are a lot in here...., transaction size etc
servers.forEach(serverExp -> {
List<CompletableFuture<Member>> memberFutures = new ArrayList<>();
serverExp.getUserInServerIds().forEach(userInAServerId -> {
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(userInAServerId);
CompletableFuture<Member> memberFuture = memberService.getMemberInServerAsync(userInAServer);
memberFutures.add(memberFuture);
});
FutureUtils.toSingleFutureGeneric(memberFutures).whenComplete((unused, throwable) -> {
self.updateFoundMembers(memberFutures, serverExp.getServerId(), resultFutures, futures);
experienceFuture.complete(null);
}).exceptionally(throwable -> {
experienceFuture.completeExceptionally(throwable);
public CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, MessageChannel messageChannel) {
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
List<Long> userIds = aUserExperiences
.stream()
.map(aUserExperience -> aUserExperience.getUser().getUserReference().getId())
.collect(Collectors.toList());
log.info("Synchronizing experience roles for {} users.", userIds.size());
CompletableFuture<Void> returnFuture = new CompletableFuture<>();
Long serverId = server.getId();
int supposedUserCount = userIds.size();
memberService.getMembersInServerAsync(server.getId(), userIds).whenComplete((members, throwable) -> {
if(throwable != null) {
log.warn("Failed to load all members in server {} for syncing experience. We started with {} and got {}.",
serverId, supposedUserCount, members.size(), throwable);
}
self.syncUsers(members, serverId, messageChannel).thenAccept(unused -> {
log.info("Finished syncing users for experience roles.");
returnFuture.complete(null);
}).exceptionally(throwable1 -> {
returnFuture.complete(null);
return null;
});
});
return experienceFuture
.thenCompose(unused -> FutureUtils.toSingleFutureGeneric(futures))
.whenComplete((unused, throwable) -> self.persistExperienceChanges(resultFutures));
return returnFuture;
}
@Transactional
public void updateFoundMembers(List<CompletableFuture<Member>> memberFutures, Long serverId, List<ExperienceGainResult> resultFutures, List<CompletableFuture<RoleCalculationResult>> futures) {
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
SystemConfigProperty defaultExpMultiplier = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY);
SystemConfigProperty defaultMinExp = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MIN_EXP_KEY);
SystemConfigProperty defaultMaxExp = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MAX_EXP_KEY);
AServer server = serverManagementService.loadOrCreate(serverId);
int minExp = configService.getLongValue(ExperienceFeatureConfig.MIN_EXP_KEY, serverId, defaultMinExp.getLongValue()).intValue();
int maxExp = configService.getLongValue(ExperienceFeatureConfig.MAX_EXP_KEY, serverId, defaultMaxExp.getLongValue()).intValue();
Double multiplier = configService.getDoubleValue(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId, defaultExpMultiplier.getDoubleValue());
PrimitiveIterator.OfInt iterator = new Random().ints(memberFutures.size(), minExp, maxExp + 1).iterator();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
public CompletableFuture<Void> syncUsers(List<Member> members, Long serverId, MessageChannel messageChannel) {
AtomicInteger currentCount = new AtomicInteger();
MessageToSend status = getUserSyncStatusUpdateModel(0, members.size(), serverId);
Message statusMessage = messageService.createStatusMessage(status, messageChannel).join();
AServer server = serverManagementService.loadServer(serverId);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles.stream().map(ADisabledExpRole::getRole).collect(Collectors.toList());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
log.info("Handling {} experiences for server {}. Using {} roles.", memberFutures.size(), serverId, roles.size());
memberFutures.forEach(future -> {
if(!future.isCompletedExceptionally()) {
Integer gainedExperience = iterator.next();
gainedExperience = (int) Math.floor(gainedExperience * multiplier);
Member member = future.join();
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
if(!roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("Handling {}. The user might gain {}.", userInServerId, gainedExperience);
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
if(aUserExperienceOptional.isPresent()) {
AUserExperience aUserExperience = aUserExperienceOptional.get();
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
log.debug("User {} will gain experience.", userInServerId);
Long newExperienceCount = aUserExperience.getExperience() + gainedExperience.longValue();
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
CompletableFuture<RoleCalculationResult> resultFuture = updateUserRole(aUserExperience, roles, newLevel.getLevel());
Long newMessageCount = aUserExperience.getMessageCount() + 1L;
ExperienceGainResult calculationResult =
ExperienceGainResult
.builder()
.calculationResult(resultFuture)
.newExperience(newExperienceCount)
.newMessageCount(newMessageCount)
.newLevel(newLevel.getLevel())
.serverId(serverId)
.userInServerId(userInAServer.getUserInServerId())
.build();
resultFutures.add(calculationResult);
futures.add(resultFuture);
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
}
} else {
log.info("User experience for user {} was not found. Planning to create new instance.", userInAServer.getUserInServerId());
Long newExperience = gainedExperience.longValue();
AExperienceLevel newLevel = calculateLevel(levels, newExperience);
Long newMessageCount = 1L;
CompletableFuture<RoleCalculationResult> resultFuture = applyInitialRole(userInAServer, roles, newLevel.getLevel());
ExperienceGainResult calculationResult =
ExperienceGainResult
.builder()
.calculationResult(resultFuture)
.newExperience(newExperience)
.newMessageCount(newMessageCount)
.newLevel(newLevel.getLevel())
.createUserExperience(true)
.userInServerId(userInAServer.getUserInServerId())
.build();
resultFutures.add(calculationResult);
futures.add(resultFuture);
}
} else {
log.debug("User {} has a role which makes the user unable to gain experience.", userInAServer.getUserInServerId());
}
}
List<CompletableFuture<Void>> futures = members
.stream()
.map(member -> this.syncUser(member, roles)
.thenAccept(unused -> {
currentCount.set(currentCount.get() + 1);
log.debug("Finished synchronizing {} users.", currentCount.get());
if(currentCount.get() % 50 == 0) {
log.info("Notifying for {} current users with synchronize.", currentCount.get());
MessageToSend newStatus = getUserSyncStatusUpdateModel(currentCount.get(), members.size(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
}
}))
.collect(Collectors.toList());
return FutureUtils.toSingleFutureGeneric(futures).thenAccept(unused -> {
MessageToSend newStatus = getUserSyncStatusUpdateModel(currentCount.get(), members.size(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
});
}
/**
* Calculates the appropriate {@link AExperienceRole experienceRole} based on the current level and awards the referenced the {@link AUserInAServer userinAServer}
* the {@link net.dv8tion.jda.api.entities.Role role}. If the user already left the guild, this will not award a {@link net.dv8tion.jda.api.entities.Role}, but just
* return the instance in order to be persisted
* @param aUserInAServer The {@link AUserInAServer userInAServer} which does not have a {@link AUserExperience userExperience} object,
* therefore we need to calculate the appropriate role and award the role
* @param roles A list of {@link AExperienceRole experienceRoles} representing the configuration which is used to calculate the appropriate
* {@link AExperienceRole experienceRole}
* @param currentLevel The current level of the user which was reached.
* @return A {@link CompletableFuture future} containing the {@link RoleCalculationResult result} of the role calculation, which completes after the user has been awarded the role.
*/
private CompletableFuture<RoleCalculationResult> applyInitialRole(AUserInAServer aUserInAServer, List<AExperienceRole> roles, Integer currentLevel) {
AExperienceRole role = experienceRoleService.calculateRole(roles, currentLevel);
if(role == null) {
log.debug("No experience role calculated. Applying none to user {} in server {}.",
aUserInAServer.getUserReference().getId(), aUserInAServer.getServerReference().getId());
return CompletableFuture.completedFuture(RoleCalculationResult
.builder()
.userInServerId(aUserInAServer.getUserInServerId())
.experienceRoleId(null)
.build());
}
Long experienceRoleId = role.getId();
Long userInServerId = aUserInAServer.getUserInServerId();
log.debug("Applying {} as the first experience role for user {} in server {}.",
experienceRoleId, aUserInAServer.getUserReference().getId(), aUserInAServer.getServerReference().getId());
return roleService.addRoleToUserAsync(aUserInAServer, role.getRole()).thenApply(aVoid -> RoleCalculationResult
.builder()
.experienceRoleId(experienceRoleId)
.userInServerId(userInServerId)
.build());
public CompletableFuture<Void> syncUser(Member member, List<AExperienceRole> roles) {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
AUserExperience userExperience = userExperienceManagementService.findByUserInServerId(aUserInAServer.getUserInServerId());
return calculateAndApplyExperienceRole(userExperience, member, roles);
}
/**
* Persists the list of {@link ExperienceGainResult results} in the database. If the creation of {@link AUserExperience userExperience} object was requested,
* this will happen here, also the correct level is selected
* @param resultFutures A list of {@link ExperienceGainResult results} which define what should be changed for the given {@link AUserExperience userExperience} object:
* The level, experience, experienceRole, message account could change, or the object could not even exist
*/
@Transactional
public void persistExperienceChanges(List<ExperienceGainResult> resultFutures) {
// we do have the _value_ of the level, but we require the actual instance
Map<Integer, AExperienceLevel> levels = experienceLevelManagementService.getLevelConfigAsMap();
log.info("Storing {} experience gain results.", resultFutures.size());
HashMap<Long, List<AExperienceRole>> serverRoleMapping = new HashMap<>();
resultFutures.forEach(experienceGainResult -> {
AUserInAServer user = userInServerManagementService.loadOrCreateUser(experienceGainResult.getUserInServerId());
AUserExperience userExperience;
if(experienceGainResult.isCreateUserExperience()) {
userExperience = userExperienceManagementService.createUserInServer(user);
log.info("Creating new experience user for user in server {}.", experienceGainResult.getUserInServerId());
@Override
public CompletableFuture<Void> syncForSingleUser(AUserExperience userExperience, Member member) {
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(userExperience.getServer());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
return calculateAndApplyExperienceRole(userExperience, member, roles);
}
private CompletableFuture<Void> calculateAndApplyExperienceRole(AUserExperience userExperience, Member member, List<AExperienceRole> roles) {
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, userExperience.getCurrentLevel().getLevel());
Long oldRoleId = userExperience.getCurrentExperienceRole() != null && userExperience.getCurrentExperienceRole().getRole() != null ? userExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null ? calculatedNewRole.getRole().getId() : null;
userExperience.setCurrentExperienceRole(calculatedNewRole);
CompletableFuture<Void> returningFuture;
if(!Objects.equals(oldRoleId, newRoleId)) {
CompletableFuture<Void> addingFuture;
if(oldRoleId != null) {
addingFuture = roleService.removeRoleFromMemberAsync(member, oldRoleId);
} else {
userExperience = userExperienceManagementService.findByUserInServerId(experienceGainResult.getUserInServerId());
addingFuture = CompletableFuture.completedFuture(null);
}
userExperience.setMessageCount(experienceGainResult.getNewMessageCount());
userExperience.setExperience(experienceGainResult.getNewExperience());
// only search the levels if the level changed, or if there is no level currently set
AExperienceLevel currentLevel = userExperience.getCurrentLevel();
boolean userExperienceHasLevel = currentLevel != null;
if(!userExperienceHasLevel || !currentLevel.getLevel().equals(experienceGainResult.getNewLevel())) {
AExperienceLevel foundLevel = levels.get(experienceGainResult.getNewLevel());
if(foundLevel != null) {
log.info("User {} in server {} changed the level. Old level {}. New level {}.", experienceGainResult.getUserInServerId(), experienceGainResult.getServerId(), currentLevel.getLevel(), experienceGainResult.getNewLevel());
userExperience.setCurrentLevel(foundLevel);
} else {
log.warn("User {} was present, but no level matching the calculation result {} could be found.", userExperience.getUser().getUserReference().getId(), experienceGainResult.getNewLevel());
}
CompletableFuture<Void> removingFeature;
if(newRoleId != null) {
removingFeature = roleService.addRoleToMemberAsync(member, newRoleId);
} else {
removingFeature = CompletableFuture.completedFuture(null);
}
AServer server = user.getServerReference();
// "Caching" the experience roles for this server
if(!serverRoleMapping.containsKey(server.getId())) {
serverRoleMapping.put(server.getId(), experienceRoleManagementService.getExperienceRolesForServer(server));
}
RoleCalculationResult roleCalculationResult = experienceGainResult.getCalculationResult().join();
if(roleCalculationResult.getExperienceRoleId() != null) {
AExperienceRole role = experienceRoleManagementService.getExperienceRoleById(roleCalculationResult.getExperienceRoleId());
userExperience.setCurrentExperienceRole(role);
}
if(experienceGainResult.isCreateUserExperience()) {
userExperienceManagementService.saveUser(userExperience);
}
});
}
@Override
public CompletableFuture<RoleCalculationResult> updateUserRole(AUserExperience userExperience, List<AExperienceRole> roles, Integer currentLevel) {
AUserInAServer user = userExperience.getUser();
Function<Void, RoleCalculationResult> returnNullRole = aVoid -> RoleCalculationResult
.builder()
.userInServerId(user.getUserInServerId())
.experienceRoleId(null)
.build();
Long userInServerId = user.getUserInServerId();
Long serverId = user.getServerReference().getId();
log.debug("Updating experience role for user {} in server {}", user.getUserReference().getId(), serverId);
AExperienceRole role = experienceRoleService.calculateRole(roles, currentLevel);
boolean currentlyHasNoExperienceRole = userExperience.getCurrentExperienceRole() == null;
// if calculation results in no role, do not add a role
if(role == null) {
log.debug("User {} in server {} does not have an experience role, according to new calculation.",
user.getUserReference().getId(), serverId);
// if the user has a experience role currently, remove it
if(!currentlyHasNoExperienceRole && !userExperience.getCurrentExperienceRole().getRole().getDeleted()){
return roleService.removeRoleFromUserAsync(user, userExperience.getCurrentExperienceRole().getRole())
.thenApply(returnNullRole);
}
return CompletableFuture.completedFuture(returnNullRole.apply(null));
returningFuture = CompletableFuture.allOf(addingFuture, removingFeature);
} else {
returningFuture = CompletableFuture.completedFuture(null);
}
Long experienceRoleId = role.getId();
Long roleId = role.getRole().getId();
// if the new role is already the one configured in the database
Long userId = user.getUserReference().getId();
Long oldUserExperienceRoleId = currentlyHasNoExperienceRole ? 0L : userExperience.getCurrentExperienceRole().getRole().getId();
return memberService.getMemberInServerAsync(user).thenCompose(member -> {
boolean userHasRoleAlready = roleService.memberHasRole(member, roleId);
boolean userHasOldRole = false;
boolean rolesChanged = true;
if(!currentlyHasNoExperienceRole) {
userHasOldRole = roleService.memberHasRole(member, oldUserExperienceRoleId);
rolesChanged = !roleId.equals(oldUserExperienceRoleId);
}
Function<Void, RoleCalculationResult> fullResult = aVoid -> RoleCalculationResult
.builder()
.experienceRoleId(experienceRoleId)
.userInServerId(userInServerId)
.build();
// if the roles changed or
// the user does not have the new target role already
// the user still has the old role
if(!userHasRoleAlready || userHasOldRole) {
CompletableFuture<Void> removalFuture;
if(userHasOldRole && rolesChanged) {
log.info("User {} in server {} loses experience role {}.", userId, serverId, oldUserExperienceRoleId);
removalFuture = roleService.removeRoleFromMemberAsync(member, oldUserExperienceRoleId);
} else {
removalFuture = CompletableFuture.completedFuture(null);
}
CompletableFuture<Void> addRoleFuture;
if(!userHasRoleAlready) {
log.info("User {} in server {} gets a new role {} because of experience.", userId, serverId, roleId);
addRoleFuture = roleService.addRoleToMemberAsync(member, roleId);
} else {
addRoleFuture = CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(removalFuture, addRoleFuture).thenApply(fullResult);
}
// we are turning the full calculation result regardless
return CompletableFuture.completedFuture(fullResult.apply(null));
});
return returningFuture;
}
@Override
public List<CompletableFuture<RoleCalculationResult>> syncUserRoles(AServer server) {
List<CompletableFuture<RoleCalculationResult>> results = new ArrayList<>();
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
for (int i = 0; i < aUserExperiences.size(); i++) {
AUserExperience userExperience = aUserExperiences.get(i);
log.info("Synchronizing {} out of {}. User in Server {}.", i, aUserExperiences.size(), userExperience.getUser().getUserInServerId());
results.add(updateUserRole(userExperience, roles, userExperience.getCurrentLevel().getLevel()));
}
return results;
}
@Override
public CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, Long channelId) {
AChannel channel = channelManagementService.loadChannel(channelId);
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
CompletableFutureList<RoleCalculationResult> calculations = executeActionOnUserExperiencesWithFeedBack(aUserExperiences, channel, (AUserExperience experience) -> updateUserRole(experience, roles, experience.getLevelOrDefault()));
return calculations.getMainFuture().thenAccept(aVoid ->
self.syncRolesInStorage(calculations.getObjects())
);
}
/**
* Updates the actually stored experience roles in the database
* @param results The list of {@link RoleCalculationResult results} which should be applied
*/
@Transactional
public void syncRolesInStorage(List<RoleCalculationResult> results) {
HashMap<Long, AExperienceRole> experienceRoleHashMap = new HashMap<>();
results.forEach(result -> {
if(result != null) {
AUserInAServer user = userInServerManagementService.loadOrCreateUser(result.getUserInServerId());
AUserExperience userExperience = userExperienceManagementService.findUserInServer(user);
log.debug("Updating experience role for {} in server {} to {}", user.getUserInServerId(), user.getServerReference().getId(), result.getExperienceRoleId());
if(result.getExperienceRoleId() != null) {
log.debug("User experience {} gets new experience role with id {}.", userExperience.getId(), result.getExperienceRoleId());
AExperienceRole role;
if(!experienceRoleHashMap.containsKey(result.getExperienceRoleId())) {
role = experienceRoleManagementService.getExperienceRoleById(result.getExperienceRoleId());
experienceRoleHashMap.put(result.getExperienceRoleId(), role);
} else {
role = experienceRoleHashMap.get(result.getExperienceRoleId());
}
userExperience.setCurrentExperienceRole(role);
} else {
log.debug("User experience {} does not get a user experience role.", userExperience.getId());
userExperience.setCurrentExperienceRole(null);
public void addExperienceToMember(Member member, Message message) {
long serverId = member.getGuild().getIdLong();
AServer server = serverManagementService.loadOrCreate(serverId);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles
.stream()
.map(ADisabledExpRole::getRole)
.collect(Collectors.toList());
if(roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("User {} has a experience disable role in server {} - not giving any experience.", member.getIdLong(), serverId);
return;
}
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
log.debug("Handling {}. The user might gain {}.", userInServerId, gainedExperience);
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(userInAServer));
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
if(!Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel())) {
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
oldLevel);
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null ? aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if(message != null && featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
.builder()
.memberDisplay(MemberDisplay.fromMember(member))
.oldExperience(oldExperience)
.newExperience(newExperienceCount)
.newLevel(newLevel.getLevel())
.oldLevel(oldLevel)
.newRole(oldRoleId != null ? RoleDisplay.fromRole(oldRoleId) : null)
.newRole(newRoleId != null ? RoleDisplay.fromRole(newRoleId) : null)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
return null;
});
}
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(!aUserExperienceOptional.isPresent()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if(!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if(result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from {} member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
if(result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to {} member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
}
});
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
}
}
@Override
@@ -488,29 +394,6 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return userExperience;
}
@Override
public CompletableFutureList<RoleCalculationResult> executeActionOnUserExperiencesWithFeedBack(List<AUserExperience> experiences, AChannel channel, Function<AUserExperience, CompletableFuture<RoleCalculationResult>> toExecute) {
List<CompletableFuture<RoleCalculationResult>> futures = new ArrayList<>();
Long serverId = channel.getServer().getId();
MessageToSend status = getUserSyncStatusUpdateModel(0, experiences.size(), serverId);
Message statusMessage = messageService.createStatusMessage(status, channel).join();
int interval = Math.min(Math.max(experiences.size() / 10, 1), 100);
for (int i = 0; i < experiences.size(); i++) {
if((i % interval) == 1) {
log.info("Updating feedback message with new index {} out of {}.", i, experiences.size());
status = getUserSyncStatusUpdateModel(i, experiences.size(), serverId);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
}
AUserExperience userExperience = experiences.get(i);
futures.add(toExecute.apply(userExperience));
log.debug("Synchronizing {} out of {}. User in server ID {}.", i, experiences.size(), userExperience.getUser().getUserInServerId());
}
status = getUserSyncStatusUpdateModel(experiences.size(), experiences.size(), serverId);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
return new CompletableFutureList<>(futures);
}
@Override
public void disableExperienceForUser(AUserInAServer userInAServer) {
log.info("Disabling experience gain for user {} in server {}.", userInAServer.getUserReference().getId(), userInAServer.getServerReference().getId());
@@ -537,15 +420,6 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return templateService.renderEmbedTemplate("user_sync_status_message", statusModel, serverId);
}
@Override
public CompletableFuture<RoleCalculationResult> syncForSingleUser(AUserExperience userExperience) {
AUserInAServer user = userExperience.getUser();
log.info("Synchronizing for user {} in server {}.", user.getUserReference().getId(), user.getServerReference().getId());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(user.getServerReference());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
return updateUserRole(userExperience, roles, userExperience.getLevelOrDefault());
}
@Override
public LeaderBoard findLeaderBoardData(AServer server, Integer page) {
if(page <= 0) {

View File

@@ -1,27 +1,29 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.experience.model.RoleCalculationResult;
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.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.template.LevelRole;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component
@@ -34,89 +36,46 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
@Autowired
private ExperienceLevelManagementService experienceLevelService;
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private ExperienceRoleServiceBean self;
@Autowired
private RoleManagementService roleManagementService;
@Autowired
private ChannelManagementService channelManagementService;
@Autowired
private RoleService roleService;
/**
* UnSets the current configuration for the passed level, and sets the {@link ARole} to be used for this level
* in the given {@link AServer}
* @param role The {@link ARole} to set the level to
* @param level The level the {@link ARole} should be awarded at
*/
@Autowired
private TemplateService templateService;
@Autowired
private MessageService messageService;
@Override
public CompletableFuture<Void> setRoleToLevel(Role role, Integer level, Long channelId) {
Long roleId = role.getIdLong();
ARole aRoleToSet = roleManagementService.findRole(roleId);
public CompletableFuture<Void> setRoleToLevel(Role role, Integer level, GuildMessageChannel messageChannel) {
ARole aRoleToSet = roleManagementService.findRole(role.getIdLong());
List<AExperienceRole> experienceRoles = getExperienceRolesAtLevel(level, aRoleToSet.getServer());
List<ARole> rolesToUnset = experienceRoles.stream().map(AExperienceRole::getRole).collect(Collectors.toList());
List<ARole> rolesToUnset = experienceRoles
.stream()
.map(AExperienceRole::getRole)
.collect(Collectors.toList());
if(rolesToUnset.size() == 1 && rolesToUnset.contains(aRoleToSet)) {
return CompletableFuture.completedFuture(null);
}
if(!rolesToUnset.contains(aRoleToSet)) {
rolesToUnset.add(aRoleToSet);
}
AExperienceLevel experienceLevel;
if(!experienceRoles.isEmpty()) {
experienceLevel = experienceRoles.get(0).getLevel();
} else {
experienceLevel = experienceLevelService.getLevel(level);
}
AExperienceRole newExperienceRole = experienceRoleManagementService.setLevelToRole(experienceLevel, aRoleToSet);
Long newlyCreatedExperienceRoleId = newExperienceRole.getId();
CompletableFuture<Void> future = new CompletableFuture<>();
unsetRoles(rolesToUnset, channelId, newExperienceRole).thenAccept(aVoid ->
self.unsetRoleInDb(level, roleId)
).thenAccept(unused -> future.complete(null)).exceptionally(throwable -> {
self.deleteExperienceRoleViaId(newlyCreatedExperienceRoleId);
future.completeExceptionally(throwable);
return null;
});
return future;
experienceRoleManagementService.setLevelToRole(experienceLevel, aRoleToSet);
if(!rolesToUnset.isEmpty()) {
return unsetRoles(rolesToUnset, messageChannel);
} else {
return CompletableFuture.completedFuture(null);
}
}
@Transactional
public void deleteExperienceRoleViaId(Long newlyCreatedExperienceRoleId) {
AExperienceRole reLoadedRole = experienceRoleManagementService.getExperienceRoleById(newlyCreatedExperienceRoleId);
experienceRoleManagementService.unsetRole(reLoadedRole);
}
/**
* Removes all previous defined {@link AExperienceRole experienceRoles} from the given level and sets the {@link ARole}
* (defined by its ID) to the level.
* @param level The level which the {@link ARole role} should be set to
* @param roleId The ID of the {@link Role} which should have its level set
*/
@Transactional
public void unsetRoleInDb(Integer level, Long roleId) {
log.info("Unsetting role {} from level {}.", roleId, level);
AExperienceLevel experienceLevel = experienceLevelService.getLevelOptional(level).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find level %s", level)));
ARole loadedRole = roleManagementService.findRole(roleId);
experienceRoleManagementService.removeAllRoleAssignmentsForLevelInServerExceptRole(experienceLevel, loadedRole.getServer(), loadedRole);
experienceRoleManagementService.setLevelToRole(experienceLevel, loadedRole);
}
/**
* Deletes the {@link AExperienceRole} and recalculates the experience for all users which currently had the associated
* {@link net.dv8tion.jda.api.entities.Role}.
* @param role The {@link ARole} to remove from the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole}
* configuration
*/
@Override
public CompletableFuture<Void> unsetRoles(ARole role, Long feedbackChannelId) {
return unsetRoles(Arrays.asList(role), feedbackChannelId);
public CompletableFuture<Void> unsetRoles(ARole role, GuildMessageChannel messageChannel) {
return unsetRoles(Arrays.asList(role), messageChannel);
}
@Override
@@ -125,64 +84,56 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
return experienceRoleManagementService.getExperienceRolesAtLevelInServer(levelObj, server);
}
@Override
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, Long feedbackChannelId) {
return unsetRoles(rolesToUnset, feedbackChannelId, null);
}
@Override
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, Long feedbackChannelId, AExperienceRole toAdd) {
if(rolesToUnset.isEmpty()) {
return CompletableFuture.completedFuture(null);
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, GuildMessageChannel messageChannel) {
List<AExperienceRole> rolesInServer = experienceRoleManagementService.getRolesInServer(rolesToUnset);
Integer totalCount = 0;
for (AExperienceRole aExperienceRole : rolesInServer) {
totalCount += aExperienceRole.getUsers().size();
}
AServer server = rolesToUnset.get(0).getServer();
AChannel channel = channelManagementService.loadChannel(feedbackChannelId);
List<AExperienceRole> experienceRolesNecessaryToRemove = new ArrayList<>();
List<AUserExperience> usersToUpdate = new ArrayList<>();
rolesToUnset.forEach(role -> {
Optional<AExperienceRole> roleInServerOptional = experienceRoleManagementService.getRoleInServerOptional(role);
if(roleInServerOptional.isPresent()) {
AExperienceRole experienceRole = roleInServerOptional.get();
experienceRolesNecessaryToRemove.add(experienceRole);
usersToUpdate.addAll(experienceRole.getUsers());
} else {
log.info("Experience role {} is not defined in server {} - skipping unset.", role.getId(), server.getId());
AtomicInteger totalCountAtomic = new AtomicInteger(totalCount);
long serverId = messageChannel.getGuild().getIdLong();
MessageToSend status = getUserSyncStatusUpdateModel(0, totalCount, serverId);
Message statusMessage = messageService.createStatusMessage(status, messageChannel).join();
AtomicInteger atomicInteger = new AtomicInteger();
List<CompletableFuture<Void>> futures = new ArrayList<>();
rolesInServer.forEach(experienceRole -> {
experienceRole.getUsers().forEach(aUserExperience -> {
futures.add(roleService.removeRoleFromUserAsync(aUserExperience.getUser(), experienceRole.getRole()).thenAccept(unused -> {
atomicInteger.set(atomicInteger.get() + 1);
log.debug("Finished synchronizing {} users.", atomicInteger.get());
if(atomicInteger.get() % 50 == 0) {
log.info("Notifying for {} current users with synchronize.", atomicInteger.get());
MessageToSend newStatus = getUserSyncStatusUpdateModel(atomicInteger.get(), totalCountAtomic.get(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
}
}));
});
});
CompletableFuture<Void> returningFuture = new CompletableFuture<>();
experienceRoleManagementService.unsetRoles(rolesInServer);
FutureUtils.toSingleFutureGeneric(futures).whenComplete((unused, throwable) -> {
MessageToSend newStatus = getUserSyncStatusUpdateModel(atomicInteger.get(), totalCountAtomic.get(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
if(throwable != null) {
log.warn("Failed to unset role in server {}.", serverId, throwable);
}
returningFuture.complete(null);
});
log.info("Recalculating the roles for {} users, because their current role was removed from experience tracking.", usersToUpdate.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.removeIf(role1 -> experienceRolesNecessaryToRemove.stream().anyMatch(aExperienceRole -> aExperienceRole.getId().equals(role1.getId())));
if(toAdd != null) {
roles.add(toAdd);
}
roles.sort(Comparator.comparing(innerRole -> innerRole.getLevel().getLevel()));
List<Long> roleIds = experienceRolesNecessaryToRemove.stream().map(AExperienceRole::getId).collect(Collectors.toList());
if(toAdd != null) {
roleIds.removeIf(aLong -> aLong.equals(toAdd.getRole().getId()));
}
CompletableFutureList<RoleCalculationResult> calculationResults = userExperienceService.executeActionOnUserExperiencesWithFeedBack(usersToUpdate, channel,
(AUserExperience ex) -> userExperienceService.updateUserRole(ex, roles, ex.getLevelOrDefault()));
return calculationResults.getMainFuture().thenAccept(aVoid -> self.persistData(calculationResults, roleIds));
return returningFuture;
}
/**
* Stores the changed experience roles for all of the {@link AUserExperience userExperiences} which are referenced in the list of
* {@link RoleCalculationResult results}. This is only executed after a role is being "unset", which means, we also
* have to remove the existing {@link AExperienceRole experienceRole}
* @param results A list of {@link CompletableFuture futures} which each contain a {@link RoleCalculationResult result}, for the members who got
* their {@link AExperienceRole experienceRole} removed
* @param roleIds The IDs of the {@link AExperienceRole experienceRoles} which were removed from the experience roles
*/
@Transactional
public void persistData(CompletableFutureList<RoleCalculationResult> results, List<Long> roleIds) {
log.info("Persisting {} role calculation results.", results.getFutures().size());
roleIds.forEach(roleId -> {
log.info("Deleting experience role {}.", roleId);
deleteExperienceRoleViaId(roleId);
});
userExperienceService.syncRolesInStorage(results.getObjects());
private MessageToSend getUserSyncStatusUpdateModel(Integer current, Integer total, Long serverId) {
UserSyncStatusModel statusModel = UserSyncStatusModel
.builder()
.currentCount(current)
.totalUserCount(total)
.build();
return templateService.renderEmbedTemplate("user_sync_status_message", statusModel, serverId);
}
@Override
public AExperienceRole calculateRole(List<AExperienceRole> roles, Integer currentLevel) {
if(roles == null || roles.isEmpty()) {

View File

@@ -1,12 +1,9 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.experience.model.ServerExperience;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -20,18 +17,9 @@ import java.util.concurrent.locks.ReentrantLock;
@Component
public class RunTimeExperienceService {
@Autowired
private MetricService metricService;
public static final String EXPERIENCE_RUNTIME_STORAGE = "experience.runtime.storage";
private static final CounterMetric EXPERIENCE_RUNTIME_STORAGE_METRIC = CounterMetric
.builder()
.name(EXPERIENCE_RUNTIME_STORAGE)
.build();
private Map<Long, List<ServerExperience>> runtimeExperience = new HashMap<>();
private Map<Long,Map<Long, Instant>> runtimeExperience = new HashMap<>();
private static final Lock lock = new ReentrantLock();
public Map<Long, List<ServerExperience>> getRuntimeExperience() {
public Map<Long, Map<Long, Instant>> getRuntimeExperience() {
return runtimeExperience;
}
@@ -49,12 +37,16 @@ public class RunTimeExperienceService {
lock.unlock();
}
@PostConstruct
public void postConstruct() {
metricService.registerGauge(EXPERIENCE_RUNTIME_STORAGE_METRIC, runtimeExperience, serverList -> serverList.values().stream()
.mapToInt(minuteEntry -> minuteEntry.stream()
.mapToInt(individualServerList -> individualServerList.getUserInServerIds().size()).sum()).sum(),
"Number of entries in runtime experience storage");
public void cleanupRunTimeStorage() {
Instant now = Instant.now();
runtimeExperience.forEach((serverId, userInstantMap) -> {
List<Long> userIdsToRemove = new ArrayList<>();
userInstantMap.forEach((userId, instant) -> {
if(instant.isBefore(now)) {
userIdsToRemove.add(userId);
}
});
userIdsToRemove.forEach(userInstantMap::remove);
});
}
}

View File

@@ -3,7 +3,6 @@ package dev.sheldan.abstracto.experience.service.management;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.repository.ExperienceRoleRepository;
@@ -13,6 +12,7 @@ import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@Slf4j
@@ -22,7 +22,7 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
private ExperienceRoleRepository experienceRoleRepository;
@Autowired
private RoleManagementService roleManagementService;
private UserExperienceManagementService userExperienceManagementService;
/**
* Removes *all* assignments of roles for the given level
@@ -46,12 +46,30 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
experienceRoleRepository.delete(role);
}
@Override
public void unsetRoles(List<AExperienceRole> roles) {
log.info("Deleting {} roles.", roles.size());
roles.forEach(experienceRole -> {
userExperienceManagementService.removeExperienceRoleFromUsers(experienceRole);
});
experienceRoleRepository.deleteAll(roles);
}
@Override
public AExperienceRole getRoleInServer(ARole role) {
// TODO throw different exception
return this.getRoleInServerOptional(role).orElseThrow(AbstractoRunTimeException::new);
}
@Override
public List<AExperienceRole> getRolesInServer(List<ARole> roles) {
List<Long> roleIds = roles
.stream()
.map(ARole::getId)
.collect(Collectors.toList());
return experienceRoleRepository.findByRole_IdIn(roleIds);
}
@Override
public Optional<AExperienceRole> getRoleInServerOptional(ARole role) {
return experienceRoleRepository.findByRole(role);

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.exception.UserInServerNotFoundException;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
@@ -32,6 +33,11 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
return byId.orElseGet(() -> createUserInServer(aUserInAServer));
}
@Override
public void removeExperienceRoleFromUsers(AExperienceRole experienceRole) {
repository.removeExperienceRoleFromUsers(experienceRole.getId());
}
@Override
public Optional<AUserExperience> findByUserInServerIdOptional(Long userInServerId) {
return repository.findById(userInServerId);

View File

@@ -6,4 +6,11 @@ abstracto.systemConfigs.expMultiplier.name=expMultiplier
abstracto.systemConfigs.expMultiplier.doubleValue=1
abstracto.featureFlags.experience.featureName=experience
abstracto.featureFlags.experience.enabled=false
abstracto.featureFlags.experience.enabled=false
abstracto.systemConfigs.expCooldownSeconds.name=expCooldownSeconds
abstracto.systemConfigs.expCooldownSeconds.longValue=60
abstracto.featureModes.levelUpNotification.featureName=experience
abstracto.featureModes.levelUpNotification.mode=levelUpNotification
abstracto.featureModes.levelUpNotification.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="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="update/updates.xml" relativeToChangelogFile="true"/>
</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="user_experience.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,13 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="drop_user_experience_role_foreign_key">
<dropForeignKeyConstraint baseTableName="user_experience" constraintName="fk_user_experience_role" />
</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="experience-job-update_schedule">
<update tableName="scheduler_job">
<column name="cron_expression" value="0 0 * * * ?"/>
<column name="clazz" value="dev.sheldan.abstracto.experience.job.ExperienceCleanupJob"/>
<where>name='experienceJob'</where>
</update>
</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="jobs.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -8,4 +8,5 @@
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.0-experience/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.15/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.8/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -1,57 +0,0 @@
package dev.sheldan.abstracto.experience.command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import net.dv8tion.jda.api.entities.Role;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class SetExpRoleTest {
@InjectMocks
private SetExpRole testUnit;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private RoleService roleService;
@Mock
private RoleManagementService roleManagementService;
private static final Long CHANNEL_ID = 4L;
@Test
public void setExpRole() {
CommandContext noParameters = CommandTestUtilities.getNoParameters();
Role roleToChange = Mockito.mock(Role.class);
when(roleToChange.getGuild()).thenReturn(noParameters.getGuild());
Integer levelToSetTo = 4;
when(noParameters.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
CommandContext context = CommandTestUtilities.enhanceWithParameters(noParameters, Arrays.asList(levelToSetTo, roleToChange));
when(experienceRoleService.setRoleToLevel(roleToChange, levelToSetTo, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}
@Test
public void validateCommand() {
CommandConfigValidator.validateCommandConfiguration(testUnit.getConfiguration());
}
}

View File

@@ -8,6 +8,8 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -28,21 +30,19 @@ public class SyncRolesTest {
@Mock
private AUserExperienceService userExperienceService;
@Mock
private ChannelManagementService channelManagementService;
@Mock
private ServerManagementService serverManagementService;
private static final Long CHANNEL_ID = 4L;
@Mock
private GuildMessageChannel messageChannel;
@Test
public void executeCommand() {
CommandContext context = CommandTestUtilities.getNoParameters();
CommandContext context = Mockito.mock(CommandContext.class);
AServer server = Mockito.mock(AServer.class);
when(serverManagementService.loadServer(context.getGuild())).thenReturn(server);
when(context.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
when(userExperienceService.syncUserRolesWithFeedback(server, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(context.getChannel()).thenReturn(messageChannel);
when(userExperienceService.syncUserRolesWithFeedback(server, messageChannel)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}

View File

@@ -1,63 +0,0 @@
package dev.sheldan.abstracto.experience.command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class UnSetExpRoleTest {
@InjectMocks
private UnSetExpRole testUnit;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private RoleManagementService roleManagementService;
@Mock
private ExperienceRoleManagementService experienceRoleManagementService;
private static final Long CHANNEL_ID = 4L;
@Test
public void setUnSetExpRole() {
CommandContext noParameters = CommandTestUtilities.getNoParameters();
ARole changedRole = Mockito.mock(ARole.class);
CommandContext context = CommandTestUtilities.enhanceWithParameters(noParameters, Arrays.asList(changedRole));
when(context.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
ARole actualRole = Mockito.mock(ARole.class);
AServer server = Mockito.mock(AServer.class);
when(actualRole.getServer()).thenReturn(server);
when(roleManagementService.findRole(changedRole.getId())).thenReturn(actualRole);
when(experienceRoleManagementService.getRoleInServerOptional(actualRole)).thenReturn(Optional.of(Mockito.mock(AExperienceRole.class)));
when(experienceRoleService.unsetRoles(actualRole, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}
@Test
public void validateCommand() {
CommandConfigValidator.validateCommandConfiguration(testUnit.getConfiguration());
}
}

View File

@@ -1,60 +0,0 @@
package dev.sheldan.abstracto.experience.listener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageType;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class ExperienceTrackerListenerTest {
@InjectMocks
public ExperienceTrackerListener testUnit;
@Mock
private AUserExperienceService userExperienceService;
@Mock
private UserInServerManagementService userInServerManagementService;
@Mock
private MessageReceivedModel model;
@Mock
private User user;
private static final Long SERVER_ID = 4L;
private static final Long USER_ID = 5L;
@Test
public void testExperienceTracking() {
AUserInAServer userInAServer = Mockito.mock(AUserInAServer.class);
Message mockedMessage = Mockito.mock(Message.class);
when(mockedMessage.isFromGuild()).thenReturn(true);
when(mockedMessage.isWebhookMessage()).thenReturn(false);
MessageChannelUnion channel = Mockito.mock(MessageChannelUnion.class);
MessageType type = MessageType.DEFAULT;
when(mockedMessage.getType()).thenReturn(type);
when(userInServerManagementService.loadOrCreateUser(SERVER_ID, USER_ID)).thenReturn(userInAServer);
when(model.getMessage()).thenReturn(mockedMessage);
when(userExperienceService.experienceGainEnabledInChannel(channel)).thenReturn(true);
when(mockedMessage.getChannel()).thenReturn(channel);
when(model.getServerId()).thenReturn(SERVER_ID);
when(mockedMessage.getAuthor()).thenReturn(user);
when(user.getIdLong()).thenReturn(USER_ID);
testUnit.execute(model);
verify(userExperienceService, times(1)).addExperience(userInAServer);
}
}

View File

@@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.service.management.UserInServerManagementServi
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import net.dv8tion.jda.api.entities.Member;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +47,9 @@ public class JoiningUserRoleListenerTest {
@Mock
private MemberJoinModel model;
@Mock
private Member member;
private static final Long SERVER_ID = 1L;
private static final Long USER_ID = 2L;
private static final Long USER_IN_SERVER_ID = 3L;
@@ -63,7 +67,8 @@ public class JoiningUserRoleListenerTest {
public void testUserWithExperienceRejoining() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experience));
when(userExperienceService.syncForSingleUser(experience)).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceService.syncForSingleUser(experience, member)).thenReturn(CompletableFuture.completedFuture(null));
when(model.getMember()).thenReturn(member);
DefaultListenerResult result = testUnit.execute(model);
Assert.assertEquals(DefaultListenerResult.PROCESSED, result);
}
@@ -72,7 +77,7 @@ public class JoiningUserRoleListenerTest {
public void testUserWithOutExperienceRejoining() {
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.empty());
testUnit.execute(model);
verify(userExperienceService, times(0)).syncForSingleUser(any());
verify(userExperienceService, times(0)).syncForSingleUser(any(), any());
}
}

View File

@@ -1,830 +0,0 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.property.SystemConfigProperty;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.DefaultConfigManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig;
import dev.sheldan.abstracto.experience.exception.NoExperienceTrackedException;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.DisabledExpRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class AUserExperienceServiceBeanTest {
@InjectMocks
private AUserExperienceServiceBean testUnit;
@Mock
private UserExperienceManagementService userExperienceManagementService;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private ExperienceLevelManagementService experienceLevelManagementService;
@Mock
private ExperienceRoleManagementService experienceRoleManagementService;
@Mock
private ConfigService configService;
@Mock
private RoleService roleService;
@Mock
private MessageService messageService;
@Mock
private TemplateService templateService;
@Mock
private ChannelManagementService channelManagementService;
@Mock
private DisabledExpRoleManagementService disabledExpRoleManagementService;
@Mock
private MemberService memberService;
@Mock
private ServerManagementService serverManagementService;
@Mock
private UserInServerManagementService userInServerManagementService;
@Mock
private AUserExperienceServiceBean self;
@Mock
private DefaultConfigManagementService defaultConfigManagementService;
@Mock
private AUserExperience userExperience;
@Mock
private AUserExperience userExperience2;
@Mock
private AUserInAServer aUserInAServer;
@Mock
private AUserInAServer aUserInAServer2;
@Mock
private AUser user;
@Mock
private AUser user2;
@Mock
private ServerExperience serverExperience;
@Mock
private AServer server;
@Mock
private Member firstMember;
@Mock
private Member secondMember;
@Mock
private AExperienceLevel level0;
@Mock
private AExperienceLevel level1;
@Mock
private AExperienceLevel level2;
@Mock
private AExperienceLevel level3;
@Mock
private AExperienceRole experienceRole1;
@Mock
private ARole aRole1;
@Mock
private AExperienceRole experienceRole2;
@Mock
private ARole aRole2;
private List<AExperienceLevel> levels = new ArrayList<>();
private List<AExperienceRole> experienceRoles = new ArrayList<>();
private static final Long USER_IN_SERVER_ID = 4L;
private static final Long USER_ID = 8L;
private static final Long SERVER_ID = 9L;
private static final Long CHANNEL_ID = 7L;
private static final Long DEFAULT_MIN_EXP = 10L;
private static final Long DEFAULT_MAX_EXP = 25L;
private static final Double DEFAULT_EXP_MULTIPLIER = 1D;
private static final Long LOW_EXP = 50L;
private static final Long MID_EXP = 250L;
private static final Long HIGH_EXP = 500L;
private static final Long LVL_0_EXP = 0L;
private static final Long LVL_1_EXP = 100L;
private static final Long LVL_2_EXP = 200L;
private static final Long LVL_3_EXP = 300L;
private static final Integer ZERO_LVL = 0;
private static final Integer SECOND_LVL = 2;
private static final Long ROLE_ID = 4L;
private static final Long SECOND_ROLE_ID = 7L;
private static final Long MESSAGE_COUNT = 10L;
@Before
public void setup() {
this.levels = Arrays.asList(level0, level1, level2, level3);
this.experienceRoles = Arrays.asList(experienceRole1, experienceRole2);
}
@Test
public void testCalculateLevelTooLow() {
this.levels = Arrays.asList(level0, level1);
setupLevels(1);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, LOW_EXP);
Assert.assertEquals(level0, calculatedLevel);
}
@Test
public void testCalculateLevelBetweenLevels() {
this.levels = Arrays.asList(level0, level1, level2);
setupLevels(3);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, MID_EXP);
Assert.assertEquals(level2, calculatedLevel);
}
@Test
public void testCalculateLevelTooHigh() {
this.levels = Arrays.asList(level0, level1, level2, level3);
setupLevels(3);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, HIGH_EXP);
Assert.assertEquals(level3, calculatedLevel);
}
@Test
public void testUpdateUserExperienceLevelChanged() {
AUserExperience experienceToCalculate = Mockito.mock(AUserExperience.class);
when(experienceToCalculate.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(experienceToCalculate.getCurrentLevel()).thenReturn(level0);
when(level0.getLevel()).thenReturn(ZERO_LVL);
when(level2.getLevel()).thenReturn(SECOND_LVL);
setupLevels(3);
Assert.assertTrue(testUnit.updateUserLevel(experienceToCalculate, levels, MID_EXP));
verify(experienceToCalculate, times(1)).setCurrentLevel(level2);
}
@Test
public void testUpdateUserExperienceLevelNotChanged() {
AUserExperience experienceToCalculate = Mockito.mock(AUserExperience.class);
when(experienceToCalculate.getCurrentLevel()).thenReturn(level2);
when(experienceToCalculate.getExperience()).thenReturn(MID_EXP);
setupLevels(3);
Assert.assertFalse(testUnit.updateUserLevel(experienceToCalculate, levels, experienceToCalculate.getExperience()));
}
@Test
public void testHandleExperienceGainSingleUser() {
when(serverExperience.getUserInServerIds()).thenReturn(Arrays.asList(USER_IN_SERVER_ID));
when(userInServerManagementService.loadOrCreateUser(USER_IN_SERVER_ID)).thenReturn(aUserInAServer);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(serverExperience.getServerId()).thenReturn(SERVER_ID);
testUnit.handleExperienceGain(Arrays.asList(serverExperience)).join();
ArgumentCaptor<List<CompletableFuture<Member>>> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
verify(self, times(1)).updateFoundMembers(listArgumentCaptor.capture(), eq(SERVER_ID), anyList(), anyList());
Assert.assertEquals(firstMember, listArgumentCaptor.getValue().get(0).join());
}
@Test
public void testHandleExperienceMemberFailed() {
when(serverExperience.getUserInServerIds()).thenReturn(Arrays.asList(USER_IN_SERVER_ID));
when(userInServerManagementService.loadOrCreateUser(USER_IN_SERVER_ID)).thenReturn(aUserInAServer);
CompletableFuture<Member> future = new CompletableFuture<>();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(future);
future.completeExceptionally(new RuntimeException());
when(serverExperience.getServerId()).thenReturn(SERVER_ID);
testUnit.handleExperienceGain(Arrays.asList(serverExperience)).join();
ArgumentCaptor<List<CompletableFuture<Member>>> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
verify(self, times(1)).updateFoundMembers(listArgumentCaptor.capture(), eq(SERVER_ID), anyList(), anyList());
Assert.assertTrue(listArgumentCaptor.getValue().get(0).isCompletedExceptionally());
}
@Test
public void testGainExpSingleUserLvlUpOneServerWithoutRole() {
/*
* In this scenario, the user has a role before, but the config changed, and now there are no experience roles.
* Hence the user should lose the experience role.
*/
setupServerId();
setupLevels(3);
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getMessageCount()).thenReturn(MESSAGE_COUNT);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
when(userExperience.getExperience()).thenReturn(500L);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userExperience.getCurrentExperienceRole()).thenReturn(experienceRole1);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(roleService.removeRoleFromUserAsync(aUserInAServer, aRole1)).thenReturn(CompletableFuture.completedFuture(null));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
Assert.assertEquals(1, experienceResults.size());
ExperienceGainResult result = experienceResults.get(0);
Assert.assertEquals(MESSAGE_COUNT + 1, result.getNewMessageCount().longValue());
Assert.assertEquals(1, roleCalculationResults.size());
RoleCalculationResult roleCalcResult = roleCalculationResults.get(0).join();
Assert.assertNull(roleCalcResult.getExperienceRoleId());
Assert.assertEquals(USER_IN_SERVER_ID, roleCalcResult.getUserInServerId());
verify(roleService, times(1)).removeRoleFromUserAsync(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any());
}
@Test
public void testLevelUpGainingNewRoleButUserAlreadyHasRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(199L);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getLevel()).thenReturn(level1);
AExperienceRole newRole = experienceRole2;
when(aRole2.getId()).thenReturn(ROLE_ID);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToMember(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUser(any(AUserInAServer.class), any(ARole.class));
}
@Test
public void testLevelUpNotGainingNewRole() {
setupServerId();
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
when(aRole1.getId()).thenReturn(ROLE_ID);
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(500L);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
AExperienceRole newRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(newRole);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToMember(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUser(any(AUserInAServer.class), any(ARole.class));
}
@Test
public void testHandleExperienceForUserNotLevelingUpWithoutExistingRole() {
setupServerId();
when(userExperience.getCurrentExperienceRole()).thenReturn(null);
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(500L);
AExperienceRole newRole = experienceRole1;
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(aRole1.getId()).thenReturn(ROLE_ID);
setupTwoExperienceRoles();
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(false);
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUserAsync(any(AUserInAServer.class), any());
}
@Test
public void handleExpGainWithTooLittleForRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getExperience()).thenReturn(50L);
setExperienceRoleLevels();
when(userExperience.getCurrentExperienceRole()).thenReturn(null);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUserAsync(any(AUserInAServer.class), any());
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any());
}
@Test
public void testUserHasExperienceRoleButNotAnymore() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getExperience()).thenReturn(50L);
AExperienceRole previousExperienceRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(previousExperienceRole);
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
when(roleService.removeRoleFromUserAsync(eq(aUserInAServer), any())).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToUserAsync(eq(aUserInAServer), any());
verify(roleService, times(1)).removeRoleFromUserAsync(eq(aUserInAServer), any());
}
@Test
public void testHandleExperienceGainForGainDisabledForUser() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setExperienceRoleLevels();
when(userExperience.getExperienceGainDisabled()).thenReturn(true);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUserAsync(eq(aUserInAServer), any());
verify(roleService, times(0)).addRoleToUserAsync(eq(aUserInAServer), any());
}
@Test
public void testHandleExperienceGainForGainDisabledForRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setExperienceRoleLevels();
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
when(roleService.hasAnyOfTheRoles(eq(firstMember), anyList())).thenReturn(true);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUser(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToMember(eq(aUserInAServer), any(ARole.class));
}
@Test
public void testHandleExperienceForUserNotLevelingUpWithExistingRole() {
setupServerId();
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(roleService.hasAnyOfTheRoles(eq(firstMember), anyList())).thenReturn(false);
when(user.getId()).thenReturn(USER_ID);
setExperienceRoleLevels();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUser(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToMember(eq(aUserInAServer), any(ARole.class));
}
@Test
public void testSyncNoRoleUserGettingRole2() {
userExperience.setCurrentExperienceRole(null);
AExperienceRole afterRole = experienceRole1;
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole1.getId()).thenReturn(ROLE_ID);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(userExperience.getUser())).thenReturn(CompletableFuture.completedFuture(firstMember));
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testSyncUserLosingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(null);
when(roleService.removeRoleFromUserAsync(aUserInAServer, aRole1)).thenReturn(CompletableFuture.completedFuture(null));
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertNull(result.getExperienceRoleId());
}
@Test
public void testSyncUserKeepingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole1;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(experienceRole1.getId()).thenReturn(ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(userExperience.getUser())).thenReturn(CompletableFuture.completedFuture(firstMember));
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testSyncUserChangingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole2;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
when(roleService.memberHasRole(firstMember, SECOND_ROLE_ID)).thenReturn(false);
when(roleService.removeRoleFromMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(roleService.addRoleToMemberAsync(firstMember, SECOND_ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(SECOND_ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testDisablingExperienceForUser() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperienceManagementService.findUserInServer(aUserInAServer)).thenReturn(experience);
testUnit.disableExperienceForUser(aUserInAServer);
verify(experience, times(1)).setExperienceGainDisabled(true);
}
@Test
public void testEnablingExpForUser() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperienceManagementService.findUserInServer(aUserInAServer)).thenReturn(experience);
testUnit.enableExperienceForUser(aUserInAServer);
verify(experience, times(1)).setExperienceGainDisabled(false);
}
@Test
public void testFindLeaderBoardData() {
executeLeaderBoardTest(server, 1);
}
@Test
public void testFindLeaderBoardDataSecondPage() {
executeLeaderBoardTest(server, 2);
}
@Test(expected = IllegalArgumentException.class)
public void testIllegalLeaderBoardPage() {
testUnit.findLeaderBoardData(server, -1);
}
@Test
public void testSyncAllUsers() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole2;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(user.getId()).thenReturn(8L);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(userExperience.getCurrentLevel()).thenReturn(level0);
when(aUserInAServer2.getUserReference()).thenReturn(user2);
when(user2.getId()).thenReturn(9L);
when(aUserInAServer2.getServerReference()).thenReturn(server);
when(userExperience2.getUser()).thenReturn(aUserInAServer2);
when(userExperience2.getCurrentLevel()).thenReturn(level0);
when(userExperience2.getCurrentExperienceRole()).thenReturn(beforeRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(memberService.getMemberInServerAsync(aUserInAServer2)).thenReturn(CompletableFuture.completedFuture(secondMember));
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(experienceRoleService.calculateRole(experienceRoles, userExperience2.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(memberService.getMemberInServerAsync(aUserInAServer2)).thenReturn(CompletableFuture.completedFuture(secondMember));
when(roleService.memberHasRole(firstMember, SECOND_ROLE_ID)).thenReturn(false);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(roleService.memberHasRole(secondMember, SECOND_ROLE_ID)).thenReturn(true);
when(roleService.removeRoleFromMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(roleService.addRoleToMemberAsync(firstMember,SECOND_ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
List<AUserExperience> experiences = Arrays.asList(userExperience, userExperience2);
when(userExperienceManagementService.loadAllUsers(server)).thenReturn(experiences);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
List<CompletableFuture<RoleCalculationResult>> calculationFutures = testUnit.syncUserRoles(server);
verify(roleService, times(0)).removeRoleFromMemberAsync(secondMember, ROLE_ID);
verify(roleService, times(0)).addRoleToMemberAsync(secondMember, SECOND_ROLE_ID);
RoleCalculationResult firstResult = calculationFutures.get(0).join();
Assert.assertEquals(SECOND_ROLE_ID, firstResult.getExperienceRoleId());
RoleCalculationResult secondResult = calculationFutures.get(1).join();
Assert.assertEquals(SECOND_ROLE_ID, secondResult.getExperienceRoleId());
}
@Test
public void testGetRankForUser() {
int rank = 1;
AUserExperience experienceObj = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experienceObj));
LeaderBoardEntryResult leaderBoardEntryTest = Mockito.mock(LeaderBoardEntryResult.class);
when(leaderBoardEntryTest.getRank()).thenReturn(rank);
when(userExperienceManagementService.getRankOfUserInServer(experienceObj)).thenReturn(leaderBoardEntryTest);
LeaderBoardEntry rankOfUserInServer = testUnit.getRankOfUserInServer(aUserInAServer);
Assert.assertEquals(experienceObj, rankOfUserInServer.getExperience());
Assert.assertEquals(rank, rankOfUserInServer.getRank().intValue());
}
@Test(expected = NoExperienceTrackedException.class)
public void testGetRankForUserNoExperienceFound() {
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.empty());
testUnit.getRankOfUserInServer(aUserInAServer);
}
@Test
public void testGetRankWhenRankReturnsNull() {
AUserExperience experienceObj = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experienceObj));
when(userExperienceManagementService.getRankOfUserInServer(experienceObj)).thenReturn(null);
LeaderBoardEntry rankOfUserInServer = testUnit.getRankOfUserInServer(aUserInAServer);
Assert.assertEquals(experienceObj, rankOfUserInServer.getExperience());
Assert.assertEquals(0, rankOfUserInServer.getRank().intValue());
}
@Test
public void testSyncRolesWithFeedBack() {
AChannel channel = Mockito.mock(AChannel.class);
when(channel.getServer()).thenReturn(server);
when(server.getId()).thenReturn(SERVER_ID);
List<AUserExperience> experiences = getUserExperiences(25);
checkStatusMessages(server, channel, experiences, 13);
}
@Test
public void testSyncRolesWithNoUsers() {
AChannel channel = Mockito.mock(AChannel.class);
List<AUserExperience> experiences = new ArrayList<>();
when(channel.getServer()).thenReturn(server);
when(server.getId()).thenReturn(SERVER_ID);
checkStatusMessages(server, channel, experiences, 1);
}
private void checkStatusMessages(AServer server, AChannel channel, List<AUserExperience> experiences, int messageCount) {
when(userExperienceManagementService.loadAllUsers(server)).thenReturn(experiences);
MessageToSend statusMessage = Mockito.mock(MessageToSend.class);
when(templateService.renderEmbedTemplate(eq("user_sync_status_message"), any(UserSyncStatusModel.class), eq(SERVER_ID))).thenReturn(statusMessage);
long messageId = 5L;
Message statusMessageJDA = Mockito.mock(Message.class);
when(statusMessageJDA.getIdLong()).thenReturn(messageId);
when(messageService.createStatusMessage(statusMessage, channel)).thenReturn(CompletableFuture.completedFuture(statusMessageJDA));
when(channelManagementService.loadChannel(CHANNEL_ID)).thenReturn(channel);
testUnit.syncUserRolesWithFeedback(server, CHANNEL_ID);
verify(messageService, times(messageCount)).updateStatusMessage(channel, messageId, statusMessage);
}
private void setupUserInServer() {
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(user.getId()).thenReturn(USER_ID);
}
private void setupTwoExperienceRoles() {
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
}
private void setupServerId() {
when(server.getId()).thenReturn(SERVER_ID);
when(serverManagementService.loadOrCreate(SERVER_ID)).thenReturn(server);
}
private void setupServerConfig() {
when(configService.getLongValue(ExperienceFeatureConfig.MIN_EXP_KEY, SERVER_ID, DEFAULT_MIN_EXP)).thenReturn(20L);
when(configService.getLongValue(ExperienceFeatureConfig.MAX_EXP_KEY, SERVER_ID, DEFAULT_MAX_EXP)).thenReturn(50L);
when(configService.getDoubleValue(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, SERVER_ID, DEFAULT_EXP_MULTIPLIER)).thenReturn(1.2);
}
private void executeLeaderBoardTest(AServer server, Integer page) {
int pageSize = 10;
List<AUserExperience> experiences = Arrays.asList(userExperience, userExperience2);
when(userExperience.getExperience()).thenReturn(LOW_EXP);
when(userExperience.getCurrentLevel()).thenReturn(level0);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(userExperience2.getExperience()).thenReturn(MID_EXP);
when(userExperience2.getCurrentLevel()).thenReturn(level1);
when(userExperience2.getUser()).thenReturn(aUserInAServer2);
when(userExperienceManagementService.findLeaderBoardUsersPaginated(server, page - 1, pageSize)).thenReturn(experiences);
LeaderBoard leaderBoardData = testUnit.findLeaderBoardData(server, page);
page--;
List<LeaderBoardEntry> entries = leaderBoardData.getEntries();
LeaderBoardEntry firstEntry = entries.get(0);
Assert.assertEquals(LOW_EXP, firstEntry.getExperience().getExperience());
Assert.assertEquals(level0, firstEntry.getExperience().getCurrentLevel());
Assert.assertEquals(aUserInAServer, firstEntry.getExperience().getUser());
Assert.assertEquals((page * pageSize) + 1, firstEntry.getRank().intValue());
LeaderBoardEntry secondEntry = entries.get(1);
Assert.assertEquals(MID_EXP, secondEntry.getExperience().getExperience());
Assert.assertEquals(level1, secondEntry.getExperience().getCurrentLevel());
Assert.assertEquals(aUserInAServer2, secondEntry.getExperience().getUser());
Assert.assertEquals((page * pageSize) + 2, secondEntry.getRank().intValue());
Assert.assertEquals(2, entries.size());
}
private void setExperienceRoleLevels() {
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
}
private void setupLevelsAndRolesAndNoDisallowed() {
when(experienceLevelManagementService.getLevelConfig()).thenReturn(levels);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(disabledExpRoleManagementService.getDisabledRolesForServer(server)).thenReturn(new ArrayList<>());
}
private void setupDefaultConfig() {
SystemConfigProperty minExpProperty = Mockito.mock(SystemConfigProperty.class);
when(minExpProperty.getLongValue()).thenReturn(DEFAULT_MIN_EXP);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MIN_EXP_KEY)).thenReturn(minExpProperty);
SystemConfigProperty maxExpProperty = Mockito.mock(SystemConfigProperty.class);
when(maxExpProperty.getLongValue()).thenReturn(DEFAULT_MAX_EXP);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MAX_EXP_KEY)).thenReturn(maxExpProperty);
SystemConfigProperty expMultiplierProperty = Mockito.mock(SystemConfigProperty.class);
when(expMultiplierProperty.getDoubleValue()).thenReturn(DEFAULT_EXP_MULTIPLIER);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY)).thenReturn(expMultiplierProperty);
}
protected List<AUserExperience> getUserExperiences(int count) {
List<AUserExperience> experiences = new ArrayList<>();
for (int i = 0; i < count; i++) {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(experience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(aUserInAServer.getUserReference()).thenReturn(user);
experiences.add(experience);
}
return experiences;
}
private void setupLevels(int count) {
if(count >= 0) {
when(level0.getExperienceNeeded()).thenReturn(LVL_0_EXP);
}
if(count >= 1) {
when(level1.getExperienceNeeded()).thenReturn(LVL_1_EXP);
}
if(count >= 2) {
when(level2.getExperienceNeeded()).thenReturn(LVL_2_EXP);
}
if(count >= 3) {
when(level3.getExperienceNeeded()).thenReturn(LVL_3_EXP);
}
}
}

View File

@@ -53,21 +53,6 @@ public class ExperienceRoleServiceBeanTest {
private static final Long CHANNEL_ID = 4L;
private static final Long ROLE_ID = 5L;
@Test
public void testUnsetRoleInDb() {
Integer levelCount = 10;
AExperienceLevel level = Mockito.mock(AExperienceLevel.class);
ARole roleToChange = Mockito.mock(ARole.class);
when(roleToChange.getServer()).thenReturn(server);
when(experienceLevelService.getLevelOptional(levelCount)).thenReturn(Optional.of(level));
when(roleManagementService.findRole(roleToChange.getId())).thenReturn(roleToChange);
testingUnit.unsetRoleInDb(levelCount, roleToChange.getId());
verify(experienceRoleManagementService, times(1)).removeAllRoleAssignmentsForLevelInServerExceptRole(level, server, roleToChange);
verify(experienceRoleManagementService, times(1)).setLevelToRole(level, roleToChange);
verify(experienceRoleManagementService, times(0)).getExperienceRolesForServer(server);
}
@Test
public void testCalculateRoleForLevelInBetween() {
List<AExperienceRole> roles = getExperienceRoles();

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.service.ExecutorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
@Configuration
public class ExperienceExecutorConfig {
@Autowired
private ExecutorService executorService;
@Bean(name = "experienceUpdateExecutor")
public TaskExecutor experienceUpdateExecutor() {
return executorService.setupExecutorFor("experienceUpdateExecutor");
}
}

View File

@@ -2,11 +2,14 @@ package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureMode;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureMode.LEVEL_UP_NOTIFICATION;
/**
* {@link FeatureConfig} instance containing the required configuration concerning system config and post targets for
* the {@link ExperienceFeatureDefinition} feature.
@@ -26,6 +29,7 @@ public class ExperienceFeatureConfig implements FeatureConfig {
* The multiplier which is applied to each calculated gained experience
*/
public static final String EXP_MULTIPLIER_KEY = "expMultiplier";
public static final String EXP_COOLDOWN_SECONDS_KEY = "expCooldownSeconds";
@Override
public FeatureDefinition getFeature() {
@@ -37,6 +41,11 @@ public class ExperienceFeatureConfig implements FeatureConfig {
*/
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(EXP_MULTIPLIER_KEY, MIN_EXP_KEY, MAX_EXP_KEY);
return Arrays.asList(EXP_MULTIPLIER_KEY, MIN_EXP_KEY, MAX_EXP_KEY, EXP_COOLDOWN_SECONDS_KEY);
}
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(LEVEL_UP_NOTIFICATION);
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.config.FeatureMode;
import lombok.Getter;
@Getter
public enum ExperienceFeatureMode implements FeatureMode {
LEVEL_UP_NOTIFICATION("levelUpNotification");
private final String key;
ExperienceFeatureMode(String key) {
this.key = key;
}
}

View File

@@ -15,10 +15,6 @@ import java.util.concurrent.CompletableFuture;
@Setter
@Builder
public class ExperienceGainResult {
/**
* The calculation result contained in a {@link CompletableFuture future}. The future is necessary, because the calculation both calculates the new role
* and removes/adds {@link net.dv8tion.jda.api.entities.Role role} to the {@link net.dv8tion.jda.api.entities.Member member}
*/
private CompletableFuture<RoleCalculationResult> calculationResult;
/**
* The ID of the {@link dev.sheldan.abstracto.core.models.database.AUserInAServer user} for which this is the result

View File

@@ -4,20 +4,10 @@ import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* The result of calculating the appropriate {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole role} for a {@link dev.sheldan.abstracto.experience.model.database.AUserExperience user}
* in a server.
*/
@Getter
@Setter
@Builder
public class RoleCalculationResult {
/**
* The ID of the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole role} which was given to the user. Can be null, in case no role is given.
*/
private Long experienceRoleId;
/**
* The ID of a {@link dev.sheldan.abstracto.core.models.database.AUserInAServer user} for who the role was calculated for.
*/
private Long userInServerId;
private Long oldRoleId;
private Long newRoleId;
}

View File

@@ -1,27 +0,0 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* Container object to store the experience in runtime and group it together. This basically is just a list of users who were tracked by experience.
* The actual calculation of the appropriate experience amount is done later.
*/
@Getter
@Setter
@Builder
public class ServerExperience {
/**
* The ID of the {@link dev.sheldan.abstracto.core.models.database.AServer} for which this experience were collected
*/
private Long serverId;
/**
* A list of IDs of the {@link dev.sheldan.abstracto.core.models.database.AUserInAServer} which should be given experience
*/
@Builder.Default
private List<Long> userInServerIds = new ArrayList<>();
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.experience.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class LevelUpNotificationModel {
private MemberDisplay memberDisplay;
private Integer oldLevel;
private Integer newLevel;
private RoleDisplay oldRole;
private RoleDisplay newRole;
private Long oldExperience;
private Long newExperience;
}

View File

@@ -1,21 +1,17 @@
package dev.sheldan.abstracto.experience.service;
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 dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.experience.model.LeaderBoard;
import dev.sheldan.abstracto.experience.model.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.model.RoleCalculationResult;
import dev.sheldan.abstracto.experience.model.ServerExperience;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
/**
* Service providing the required mechanisms to provide experience tracking.
@@ -24,12 +20,7 @@ import java.util.function.Function;
*/
public interface AUserExperienceService {
String EXPERIENCE_GAIN_CHANNEL_GROUP_KEY = "experienceGain";
/**
* Adds the given {@link AUserInAServer userInAServer} to the list of user who gained experience in the current minute.
* Does not add the user to the list of users, if it is already in there.
* @param userInAServer The {@link AUserInAServer userInAServer} to be added to the list of users gaining experience
*/
void addExperience(AUserInAServer userInAServer);
void addExperience(Member member, Message message);
/**
* Calculates the appropriate level for the given experience amount according to the given {@link AExperienceLevel levels}
@@ -51,58 +42,9 @@ public interface AUserExperienceService {
*/
boolean updateUserLevel(AUserExperience userExperience, List<AExperienceLevel> levels, Long experienceCount);
/**
* Iterates through the given list of {@link AServer servers} and increases the experience of the users contained in the
* {@link ServerExperience serverExperience} object, also increments the level and changes the role if necessary.
* This uses the respective configurable max/minExp and multiplier for each {@link AServer server} and increases the message count
* of each user by 1.
* @param serverExp The list of {@link AServer servers} containing the users which get experience
* @return A {@link CompletableFuture future} completing when the experience gain was calculated and roles were assigned
*/
CompletableFuture<Void> handleExperienceGain(List<ServerExperience> serverExp);
CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, MessageChannel messageChannel);
/**
* Calculates the currently appropriate {@link AExperienceRole} for the given user and updates the role on the
* {@link net.dv8tion.jda.api.entities.Member} and ond the {@link AUserExperience}. Effectively synchronizes the
* state in the server and the database.
* @param userExperience The {@link AUserExperience userExperience} object to recalculate the {@link AExperienceRole experienceRole} for
* @param roles The list of {@link AExperienceRole roles} used as a role configuration
* @param currentLevel The current level of the user
* @return A {@link CompletableFuture future} containing the {@link RoleCalculationResult result} of the role calculation,
* completing after the role of the {@link net.dv8tion.jda.api.entities.Member} has been updated, if any
*/
CompletableFuture<RoleCalculationResult> updateUserRole(AUserExperience userExperience, List<AExperienceRole> roles, Integer currentLevel);
/**
* Synchronizes the state ({@link AExperienceRole}, {@link net.dv8tion.jda.api.entities.Role})
* of all the users provided in the {@link AServer} object in the {@link AUserExperience}
* and on the {@link net.dv8tion.jda.api.entities.Member} according
* to how much experience the user has. Runs completely in the background.
* @param server The {@link AServer} to update the users for
* @return The list of {@link CompletableFuture futures} for each update of the users in the {@link AServer server}
*/
List<CompletableFuture<RoleCalculationResult>> syncUserRoles(AServer server);
/**
* Synchronizes the state ({@link AExperienceRole}, {@link net.dv8tion.jda.api.entities.Role})
* of all the users provided in the {@link AServer} object in the {@link AUserExperience}
* and on the {@link net.dv8tion.jda.api.entities.Member} according
* to how much experience the user has. This method provides feedback back to the user in the provided {@link AChannel channel}
* while the process is going own.
* @param server The {@link AServer} to update users for
* @param channelId The ID of a {@link AChannel channel} in which the {@link dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel statusUpdate}
* should be posted to
* @return A {@link CompletableFuture future} which completes after all the role changes have been completed
*/
CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, Long channelId);
/**
* Recalculates the role of a single user in a server and synchronize the {@link net.dv8tion.jda.api.entities.Role}
* in the {@link net.dv8tion.jda.api.entities.Guild}
* @param userExperience The {@link AUserExperience} to synchronize the role for
* @return A {@link CompletableFuture future} which completes after the roles have been synced for the given {@link AUserInAServer user}
*/
CompletableFuture<RoleCalculationResult> syncForSingleUser(AUserExperience userExperience);
CompletableFuture<Void> syncForSingleUser(AUserExperience userExperience, Member member);
/**
* Loads the desired page of the ordered complete leaderboard from the {@link AServer} and returns the information as a {@link LeaderBoard}
@@ -122,19 +64,6 @@ public interface AUserExperienceService {
*/
LeaderBoardEntry getRankOfUserInServer(AUserInAServer userInAServer);
/**
* Provides a method to execute an action on a list of {@link AUserExperience} and provide feedback in the given {@link AChannel}
* in the form of {@link dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel} to be rendered with a certain
* template
* @param experiences The list of {@link AUserExperience} to be working on
* @param channel The {@link AChannel} used to provide feedback to the user
* @param toExecute The {@link Function} which should be executed on each element of the passed list,
* this function needs to take a {@link AUserExperience userExperience} as parameter and returns a {@link CompletableFuture}
* with a {@link RoleCalculationResult} for each of them. These futures are then returned.
* @return A {@link CompletableFutureList completeFutureList} which represents the individual {@link RoleCalculationResult results} and a primary future, which is completed after all of the individual ones are
*/
CompletableFutureList<RoleCalculationResult> executeActionOnUserExperiencesWithFeedBack(List<AUserExperience> experiences, AChannel channel, Function<AUserExperience, CompletableFuture<RoleCalculationResult>> toExecute);
/**
* Disables the experience gain for a user directly. This sets the `experienceGainDisabled` on the respective {@link AUserExperience} object to true
* @param userInAServer The {@link AUserInAServer} to disable experience gain for
@@ -147,12 +76,6 @@ public interface AUserExperienceService {
*/
void enableExperienceForUser(AUserInAServer userInAServer);
/**
* Updates the actually stored experience roles in the database
* @param results The list of {@link RoleCalculationResult} which should be updated in the database
*/
void syncRolesInStorage(List<RoleCalculationResult> results);
boolean experienceGainEnabledInChannel(MessageChannel messageChannel);
AUserExperience createUserExperienceForUser(AUserInAServer aUserInAServer, Long experience, Long messageCount);

View File

@@ -7,6 +7,7 @@ import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.template.LevelRole;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -15,34 +16,12 @@ import java.util.concurrent.CompletableFuture;
* Service providing several methods surrounding {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole experienceRole}.
*/
public interface ExperienceRoleService {
/**
* Creates an {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole experienceRole} according to the given
* parameters. This actually updates the {@link net.dv8tion.jda.api.entities.Member members}
* which currently possessed the given role before and provides a display to see how far the progress is
* @param role The {@link ARole role} to set the level to
* @param level The new level the {@link ARole role} should be awarded at
* @param channelId The ID of the {@link dev.sheldan.abstracto.core.models.database.AChannel} in which the status updates
* should be sent to
* @return A {@link CompletableFuture future} which completes, after all the updates on the {@link net.dv8tion.jda.api.entities.Member}
* have been completed
*/
CompletableFuture<Void> setRoleToLevel(Role role, Integer level, Long channelId);
/**
* Removes the role from the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole} configuration,
* this will also update all the {@link net.dv8tion.jda.api.entities.Member} which previously had this role and re-calculates
* a new {@link AExperienceRole experienceRole} for them while also updating them in the guild
* @param role The {@link ARole} to remove from the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole}
* configuration
* @param channelId The ID of the {@link dev.sheldan.abstracto.core.models.database.AChannel} in which the status updates
* should be sent to
* @return A {@link CompletableFuture future} which completes, after all the updates on the {@link net.dv8tion.jda.api.entities.Member}
* have been completed
*/
CompletableFuture<Void> unsetRoles(ARole role, Long channelId);
CompletableFuture<Void> setRoleToLevel(Role role, Integer level, GuildMessageChannel messageChannel);
CompletableFuture<Void> unsetRoles(ARole role, GuildMessageChannel messageChannel);
List<AExperienceRole> getExperienceRolesAtLevel(Integer level, AServer server);
CompletableFuture<Void> unsetRoles(List<ARole> roles, Long channelId);
CompletableFuture<Void> unsetRoles(List<ARole> roles, Long channelId, AExperienceRole toAdd);
CompletableFuture<Void> unsetRoles(List<ARole> roles, GuildMessageChannel messageChannel);
/**
* Calculates the appropriate {@link AExperienceRole experienceRole} based on the provided list of {@link AExperienceRole experienceRole}

View File

@@ -37,6 +37,7 @@ public interface ExperienceRoleManagementService {
* @param role The {@link AExperienceRole experienceRole} to delete.
*/
void unsetRole(AExperienceRole role);
void unsetRoles(List<AExperienceRole> role);
/**
* Retrieves the {@link AExperienceRole experienceRole} which uses the given {@link ARole role} in the {@link AServer server}
@@ -44,6 +45,7 @@ public interface ExperienceRoleManagementService {
* @return the {@link AExperienceRole experienceRole} which uses the given {@link ARole role}
*/
AExperienceRole getRoleInServer(ARole role);
List<AExperienceRole> getRolesInServer(List<ARole> role);
/**
* Retrieves a possible {@link AExperienceRole role}, if it exists, for the given {@link ARole}. Returns an empty Optional if it does not exist

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.experience.service.management;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
@@ -19,6 +20,7 @@ public interface UserExperienceManagementService {
* @return The {@link AUserExperience userExperience} object representing the {@link AUserInAServer userInAServer}
*/
AUserExperience findUserInServer(AUserInAServer aUserInAServer);
void removeExperienceRoleFromUsers(AExperienceRole experienceRole);
/**
* Retrieves a possible {@link AUserExperience userExperience} for the given ID of the {@link AUserInAServer}.

View File

@@ -29,13 +29,6 @@ public class JoinLogger implements AsyncJoinListener {
@Autowired
private PostTargetService postTargetService;
@Autowired
private MemberService memberService;
@Autowired
private JoinLogger self;
@Override
public DefaultListenerResult execute(MemberJoinModel listenerModel) {
MemberJoinLogModel model = MemberJoinLogModel

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.logging.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncLeaveListener;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.listener.MemberLeaveModel;
import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.PostTargetService;
@@ -27,13 +28,6 @@ public class LeaveLogger implements AsyncLeaveListener {
@Autowired
private PostTargetService postTargetService;
@Autowired
private MemberService memberService;
@Autowired
private LeaveLogger self;
@Override
public FeatureDefinition getFeature() {
return LoggingFeatureDefinition.LOGGING;
@@ -41,8 +35,14 @@ public class LeaveLogger implements AsyncLeaveListener {
@Override
public DefaultListenerResult execute(MemberLeaveModel listenerModel) {
ServerUser leavingUser = ServerUser
.builder()
.userId(listenerModel.getUser().getIdLong())
.serverId(listenerModel.getServerId())
.build();
MemberLeaveModel model = MemberLeaveModel
.builder()
.leavingUser(leavingUser)
.user(listenerModel.getUser())
.build();
log.debug("Logging leave event for user {} in server {}.", listenerModel.getUser().getIdLong(), listenerModel.getServerId());

View File

@@ -21,6 +21,14 @@ public class RoleDisplay {
.build();
}
public static RoleDisplay fromRole(Long roleId) {
return RoleDisplay
.builder()
.roleId(roleId)
.roleMention("<@&" + roleId + '>')
.build();
}
public static RoleDisplay fromARole(ARole role) {
return RoleDisplay
.builder()