[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>