added javadoc to experience tracking module

changed persistence configuration for entities, so cascade is more correct
changed the way channels get their server assigned
added feature to only change the exp role of a user, if he doesn't already have the new target role
added the name of the needed feature, to the message indicating that a feature has been disabled
updated unsetExpRole to have a status update message, because there might be a lot of users which need a role update
This commit is contained in:
Sheldan
2020-04-23 23:39:05 +02:00
parent 1a74924850
commit cf37d4adef
69 changed files with 929 additions and 260 deletions

View File

@@ -20,6 +20,7 @@ import java.util.List;
public class ExpScale extends AbstractConditionableCommand {
public static final String EXP_MULTIPLIER_KEY = "expMultiplier";
@Autowired
private ConfigService configService;

View File

@@ -13,7 +13,7 @@ import dev.sheldan.abstracto.experience.converter.LeaderBoardModelConverter;
import dev.sheldan.abstracto.experience.models.LeaderBoard;
import dev.sheldan.abstracto.experience.models.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.models.templates.LeaderBoardModel;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.templating.model.MessageToSend;
import dev.sheldan.abstracto.templating.service.TemplateService;
@@ -30,7 +30,7 @@ public class LeaderBoardCommand extends AbstractConditionableCommand {
public static final String LEADERBOARD_POST_EMBED_TEMPLATE = "leaderboard_post";
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@@ -48,12 +48,13 @@ public class LeaderBoardCommand extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
// parameter is optional, in case its not present, we default to the 0th page
Integer page = parameters.size() > 0 ? (Integer) parameters.get(0) : 0;
LeaderBoard leaderBoard = experienceTrackerService.findLeaderBoardData(commandContext.getUserInitiatedContext().getServer(), page);
LeaderBoard leaderBoard = userExperienceService.findLeaderBoardData(commandContext.getUserInitiatedContext().getServer(), page);
LeaderBoardModel leaderBoardModel = (LeaderBoardModel) ContextConverter.fromCommandContext(commandContext, LeaderBoardModel.class);
leaderBoardModel.setUserExperiences(converter.fromLeaderBoard(leaderBoard));
LeaderBoardEntry userRank = experienceTrackerService.getRankOfUserInServer(commandContext.getUserInitiatedContext().getAUserInAServer());
LeaderBoardEntry userRank = userExperienceService.getRankOfUserInServer(commandContext.getUserInitiatedContext().getAUserInAServer());
leaderBoardModel.setUserExecuting(converter.fromLeaderBoardEntry(userRank));
MessageToSend messageToSend = templateService.renderEmbedTemplate(LEADERBOARD_POST_EMBED_TEMPLATE, leaderBoardModel);
channelService.sendMessageToEndInTextChannel(messageToSend, commandContext.getChannel());

View File

@@ -13,7 +13,7 @@ import dev.sheldan.abstracto.experience.models.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.models.database.AUserExperience;
import dev.sheldan.abstracto.experience.models.templates.RankModel;
import dev.sheldan.abstracto.experience.service.ExperienceLevelService;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.templating.model.MessageToSend;
import dev.sheldan.abstracto.templating.service.TemplateService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,6 +25,7 @@ import java.util.List;
@Component
public class Rank extends AbstractConditionableCommand {
public static final String RANK_POST_EMBED_TEMPLATE = "rank_post";
@Autowired
private LeaderBoardModelConverter converter;
@@ -32,7 +33,7 @@ public class Rank extends AbstractConditionableCommand {
private TemplateService templateService;
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Autowired
private ExperienceLevelService experienceLevelService;
@@ -40,11 +41,11 @@ public class Rank extends AbstractConditionableCommand {
@Override
public CommandResult execute(CommandContext commandContext) {
RankModel rankModel = (RankModel) ContextConverter.fromCommandContext(commandContext, RankModel.class);
LeaderBoardEntry userRank = experienceTrackerService.getRankOfUserInServer(commandContext.getUserInitiatedContext().getAUserInAServer());
LeaderBoardEntry userRank = userExperienceService.getRankOfUserInServer(commandContext.getUserInitiatedContext().getAUserInAServer());
rankModel.setRankUser(converter.fromLeaderBoardEntry(userRank));
AUserExperience experienceObj = userRank.getExperience();
rankModel.setExperienceToNextLevel(experienceLevelService.calculateExperienceToNextLevel(experienceObj.getCurrentLevel().getLevel(), experienceObj.getExperience()));
MessageToSend messageToSend = templateService.renderEmbedTemplate("rank_post", rankModel);
MessageToSend messageToSend = templateService.renderEmbedTemplate(RANK_POST_EMBED_TEMPLATE, rankModel);
channelService.sendMessageToEndInTextChannel(messageToSend, commandContext.getChannel());
return CommandResult.fromSuccess();

View File

@@ -41,11 +41,11 @@ public class SetExpRole extends AbstractConditionableCommand {
Long roleId = (Long) commandContext.getParameters().getParameters().get(1);
ARole role = roleManagementService.findRole(roleId);
AServer server = commandContext.getUserInitiatedContext().getServer();
if(!roleService.isRoleInServer(server, role)) {
if(!roleService.isRoleInServer(role)) {
throw new RoleException("Role not found.");
}
log.info("Setting role {} to be used for level {} on server {}", roleId, level, server.getId());
experienceRoleService.setRoleToLevel(role, level, server);
experienceRoleService.setRoleToLevel(role, level, server, commandContext.getUserInitiatedContext().getChannel());
return CommandResult.fromSuccess();
}

View File

@@ -8,7 +8,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.config.ExperienceFeatures;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -21,13 +21,13 @@ import java.util.List;
public class SyncRoles extends AbstractConditionableCommand {
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Override
public CommandResult execute(CommandContext commandContext) {
AServer server = commandContext.getUserInitiatedContext().getServer();
log.info("Synchronizing roles on server {}", server.getId());
experienceTrackerService.syncUserRolesWithFeedback(server, commandContext.getUserInitiatedContext().getChannel());
userExperienceService.syncUserRolesWithFeedback(server, commandContext.getUserInitiatedContext().getChannel());
return CommandResult.fromSuccess();
}

View File

@@ -29,7 +29,9 @@ public class UnSetExpRole extends AbstractConditionableCommand {
public CommandResult execute(CommandContext commandContext) {
Long roleId = (Long) commandContext.getParameters().getParameters().get(0);
ARole role = roleManagementService.findRole(roleId);
experienceRoleService.unsetRole(role, commandContext.getUserInitiatedContext().getServer());
// do not check for the existence of the role, because if the role was deleted, users should be able
// to get rid of it in the configuration
experienceRoleService.unsetRole(role, commandContext.getUserInitiatedContext().getServer(), commandContext.getUserInitiatedContext().getChannel());
return CommandResult.fromSuccess();
}

View File

@@ -10,8 +10,23 @@ import org.springframework.stereotype.Component;
@Setter
@ConfigurationProperties(prefix = "abstracto.experience")
public class ExperienceConfig {
/**
* The default min experience range from the properties file. This is used, when the bot joins a new guild.
*/
private Integer minExp;
/**
* The default max experience range from the properties file. This is used, when the bot joins a new guild.
*/
private Integer maxExp;
/**
* The default multiplier from the properties file. This is used, when the bot joins a new guild.
*/
private Integer expMultiplier;
/**
* The defaul maxLevel from the properties file. This configuration applies globally, as the amount of levels does not depend on the server.
*/
private Integer maxLvl;
}

View File

@@ -7,6 +7,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Listener responsible to configure the required experience configurations in case the bot joins a new server.
*/
@Component
@Slf4j
public class ExperienceConfigListener implements ServerConfigListener {

View File

@@ -7,6 +7,10 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* Component responsible to create the amount of {@link dev.sheldan.abstracto.experience.models.database.AExperienceLevel}
* configured in the {@link ExperienceConfig}. This is executed when the application starts up.
*/
@Component
@Slf4j
public class ExperienceLevelLoader {
@@ -20,12 +24,7 @@ public class ExperienceLevelLoader {
@EventListener
public void handleContextRefreshEvent(ContextRefreshedEvent ctxStartEvt) {
Integer maxLevel = experienceConfig.getMaxLvl();
Long experience = 0L;
log.info("Setting up experience level configuration.");
experienceLevelService.createExperienceLevel(0, 0L);
for (int i = 1; i < maxLevel; i++) {
experience = experience + experienceLevelService.calculateExperienceForLevel(i - 1);
experienceLevelService.createExperienceLevel(i, experience);
}
experienceLevelService.createLevelsUntil(maxLevel);
}
}

View File

@@ -12,12 +12,22 @@ import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Converter used to conver from {@link LeaderBoard} to a list of {@link LeaderBoardEntryModel}
*/
@Component
public class LeaderBoardModelConverter {
@Autowired
private BotService botService;
/**
* Converts the complete {@link LeaderBoard} into a list of {@link LeaderBoardEntryModel} which contain additional
* information available for rendering the leaderboard ({@link Member} reference)
* @param leaderBoard The {@link LeaderBoard} object to be converted
* @return The list of {@link LeaderBoardEntryModel} which contain the fully fledged information provided to the
* leaderboard template
*/
public List<LeaderBoardEntryModel> fromLeaderBoard(LeaderBoard leaderBoard) {
List<LeaderBoardEntryModel> models = new ArrayList<>();
leaderBoard.getEntries().forEach(leaderBoardEntry -> {
@@ -27,6 +37,13 @@ public class LeaderBoardModelConverter {
return models;
}
/**
* Converts the given {@link LeaderBoardEntry} to a {@link LeaderBoardEntryModel}, which provides a reference to the
* {@link Member} object of the given {@link AUserInAServer} for convenience in the template
* @param leaderBoardEntry The {@link LeaderBoardEntry} to be converted
* @return The {@link LeaderBoardEntryModel} accompanied with the {@link Member} reference, might be null, if the
* user left the guild
*/
public LeaderBoardEntryModel fromLeaderBoardEntry(LeaderBoardEntry leaderBoardEntry) {
AUserInAServer entryUser = leaderBoardEntry.getExperience().getUser();
Member entryMember = botService.getMemberInServer(entryUser.getServerReference().getId(), entryUser.getUserReference().getId());

View File

@@ -1,7 +1,7 @@
package dev.sheldan.abstracto.experience.job;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
@@ -16,6 +16,11 @@ import java.util.HashMap;
import java.util.List;
/**
* This {@link QuartzJobBean} is executed regularly and calls the the {@link AUserExperienceService}
* store the tracked experience from runtime. This job also cleans up the already processed entries in the runtime
* experience.
*/
@Slf4j
@DisallowConcurrentExecution
@Component
@@ -23,16 +28,16 @@ import java.util.List;
public class ExperiencePersistingJob extends QuartzJobBean {
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
HashMap<Long, List<AServer>> runtimeExperience = experienceTrackerService.getRuntimeExperience();
HashMap<Long, List<AServer>> runtimeExperience = userExperienceService.getRuntimeExperience();
log.info("Running experience persisting job.");
Long pastMinute = (Instant.now().getEpochSecond() / 60) - 1;
if(runtimeExperience.containsKey(pastMinute)) {
log.info("Found experience to persist.");
experienceTrackerService.handleExperienceGain(runtimeExperience.get(pastMinute));
userExperienceService.handleExperienceGain(runtimeExperience.get(pastMinute));
runtimeExperience.remove(pastMinute);
}
}

View File

@@ -4,18 +4,22 @@ import dev.sheldan.abstracto.core.listener.MessageReceivedListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.UserManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatures;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* This {@link MessageReceivedListener} is responsible to execute the {@link AUserExperienceService} in order to track
* that a certain user has send a message, and experience should be awarded.
*/
@Component
public class ExperienceTrackerListener implements MessageReceivedListener {
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Autowired
private UserManagementService userManagementService;
@@ -24,7 +28,7 @@ public class ExperienceTrackerListener implements MessageReceivedListener {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void execute(Message message) {
AUserInAServer cause = userManagementService.loadUser(message.getMember());
experienceTrackerService.addExperience(cause);
userExperienceService.addExperience(cause);
}
@Override

View File

@@ -5,7 +5,7 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.UserManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatures;
import dev.sheldan.abstracto.experience.models.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.ExperienceTrackerService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
@@ -13,6 +13,10 @@ import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* If a user joins, this {@link JoinListener} retrieves the previously stored {@link AUserExperience} and gives the
* {@link Member} the necessary {@link net.dv8tion.jda.api.entities.Role} according to the current configuration
*/
@Component
@Slf4j
public class JoiningUserRoleListener implements JoinListener {
@@ -24,14 +28,14 @@ public class JoiningUserRoleListener implements JoinListener {
private UserManagementService userManagementService;
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Override
public void execute(Member member, Guild guild, AUserInAServer aUserInAServer) {
AUserExperience userExperience = userExperienceManagementService.findUserInServer(aUserInAServer);
if(userExperience != null) {
log.info("User {} joined {} with previous experience. Setting up experience role again (if necessary).", member.getUser().getIdLong(), guild.getIdLong());
experienceTrackerService.syncForSingleUser(userExperience);
userExperienceService.syncForSingleUser(userExperience);
}
}

View File

@@ -4,7 +4,9 @@ import dev.sheldan.abstracto.experience.models.database.AExperienceLevel;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Repository to manage the access to the table managed by {@link AExperienceLevel}
*/
@Repository
public interface ExperienceLevelRepository extends JpaRepository<AExperienceLevel, Integer> {
AExperienceLevel findTopByExperienceNeededGreaterThanOrderByExperienceNeededAsc(Long experience);
}

View File

@@ -9,9 +9,32 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the access to the table managed by {@link AExperienceRole}
*/
@Repository
public interface ExperienceRoleRepository extends JpaRepository<AExperienceRole, Long> {
/**
* Finds the {@link AExperienceRole} of the given {@link AServer} and {@link ARole}
* @param server The {@link AServer} to retrieve the {@link AExperienceRole} for
* @param role The {@link ARole} to filter for
* @return The {@link AExperienceRole} found or null if the query did not return any results
*/
AExperienceRole findByRoleServerAndRole(AServer server, ARole role);
/**
* Finds a list of {@link AExperienceRole} (if there are multiple ones, because of misconfiguration) of the given
* {@link AExperienceLevel} and {@link AServer}
* @param level The {@link AExperienceLevel} to search for
* @param server The {@link AServer} to search for
* @return The list of {@link AExperienceRole} found by the given parameters
*/
List<AExperienceRole> findByLevelAndRoleServer(AExperienceLevel level, AServer server);
/**
* Finds all {@link AExperienceRole} of the given {@link AServer}
* @param server The {@link AServer} to load the list of {@link AExperienceRole} for
* @return A list of {@link AExperienceRole} configured to be used on the given {@link AServer}
*/
List<AExperienceRole> findByRoleServer(AServer server);
}

View File

@@ -1,7 +1,7 @@
package dev.sheldan.abstracto.experience.repository;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.experience.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.database.AUserExperience;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -11,11 +11,34 @@ import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository to manage the access to the table managed by {@link AUserExperience}
*/
@Repository
public interface UserExperienceRepository extends JpaRepository<AUserExperience, Long> {
/**
* Finds all {@link AUserExperience} of the given {@link AServer}
* @param server The {@link AServer} to retriev ethe {@link AUserExperience} for
* @return A complete list of {@link AUserExperience} of the given {@link AServer}
*/
List<AUserExperience> findByUser_ServerReference(AServer server);
/**
* Retrieves the {@link AUserExperience} ordered by experience, and applies the {@link Pageable} to only filter out certain pages.
* @param server The {@link AServer} to retrieve the {@link AUserExperience} information for
* @param pageable A {@link Pageable} object to indicate the pages which should be retrieved, pagesize is 10
* @return A list of {@link AUserExperience} of the given {@link AServer} ordered by the experience of the users, paginated by the given
* configuration
*/
List<AUserExperience> findTop10ByUser_ServerReferenceOrderByExperienceDesc(AServer server, Pageable pageable);
/**
* This returns the {@link LeaderBoardEntryResult} object containing the information about the rank of a user in a server.
* @param id The {@link dev.sheldan.abstracto.core.models.database.AUserInAServer} id to search for
* @return the {@link LeaderBoardEntryResult} of this {@link dev.sheldan.abstracto.core.models.database.AUserInAServer}
* containing rank and experience information
*/
@Query(value = "WITH user_experience_ranked AS" +
"( " +
" SELECT id, experience, experience_role_id, level_id, message_count, ROW_NUMBER() OVER ( ORDER BY experience DESC ) " +

View File

@@ -3,10 +3,11 @@ 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.service.BotService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.experience.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.LeaderBoard;
import dev.sheldan.abstracto.experience.models.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.models.database.AExperienceLevel;
@@ -19,6 +20,7 @@ import dev.sheldan.abstracto.experience.service.management.UserExperienceManagem
import dev.sheldan.abstracto.templating.model.MessageToSend;
import dev.sheldan.abstracto.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -27,10 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@Component
@Slf4j
public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
public class AUserExperienceServiceBean implements AUserExperienceService {
private HashMap<Long, List<AServer>> runtimeExperience = new HashMap<>();
@@ -40,6 +43,9 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
@Autowired
private ExperienceLevelService experienceLevelService;
@Autowired
private ExperienceRoleService experienceRoleService;
@Autowired
private ExperienceLevelManagementService experienceLevelManagementService;
@@ -58,6 +64,13 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
@Autowired
private TemplateService templateService;
@Autowired
private BotService botService;
/**
* Creates the user in the runtime experience, if the user was not in yet. Also creates an entry for the minute, if necessary.
* @param userInAServer The {@link AUserInAServer} to be added to the list of users gaining experience
*/
@Override
public void addExperience(AUserInAServer userInAServer) {
log.trace("Adding experience for user {} in server {}", userInAServer.getUserReference().getId(), userInAServer.getServerReference().getId());
@@ -85,11 +98,17 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
return runtimeExperience;
}
/**
* Calculates the level of the given {@link AUserExperience} accoring to the given {@link AExperienceLevel} list
* @param experience The {@link AUserExperience} to calculate the level for
* @param levels The list of {@link AExperienceLevel} representing the level configuration
* @return The appropriate level according to the level config
*/
@Override
public Integer calculateLevel(AUserExperience userInAServer, List<AExperienceLevel> levels) {
public Integer calculateLevel(AUserExperience experience, List<AExperienceLevel> levels) {
AExperienceLevel lastLevel = levels.get(0);
for (AExperienceLevel level : levels) {
if(level.getExperienceNeeded() >= userInAServer.getExperience()) {
if(level.getExperienceNeeded() >= experience.getExperience()) {
return lastLevel.getLevel();
} else {
lastLevel = level;
@@ -99,34 +118,26 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
}
@Override
public AExperienceRole calculateRole(AUserExperience userInAServer, List<AExperienceRole> roles) {
if(roles.size() == 0) {
return null;
}
AExperienceRole lastRole = null;
for (AExperienceRole experienceRole : roles) {
if(userInAServer.getCurrentLevel().getLevel() >= experienceRole.getLevel().getLevel()) {
lastRole = experienceRole;
} else {
return lastRole;
}
}
return lastRole;
}
@Override
public void increaseExpForUser(AUserExperience userInAServer, Long experience, List<AExperienceLevel> levels) {
AUserInAServer user = userInAServer.getUser();
log.trace("Increasing experience for user {} in server {} by {}.", user.getUserReference().getId(), user.getServerReference().getId(), experience);
userInAServer.setExperience(userInAServer.getExperience() + experience);
Integer correctLevel = calculateLevel(userInAServer, levels);
Integer currentLevel = userInAServer.getCurrentLevel() != null ? userInAServer.getCurrentLevel().getLevel() : 0;
public boolean updateUserlevel(AUserExperience userExperience, List<AExperienceLevel> levels) {
AUserInAServer user = userExperience.getUser();
Integer correctLevel = calculateLevel(userExperience, levels);
Integer currentLevel = userExperience.getCurrentLevel() != null ? userExperience.getCurrentLevel().getLevel() : 0;
if(!correctLevel.equals(currentLevel)) {
log.info("User {} leveled from {} to {}", user.getUserReference().getId(), currentLevel, correctLevel);
userInAServer.setCurrentLevel(experienceLevelManagementService.getLevel(correctLevel));
userExperience.setCurrentLevel(experienceLevelManagementService.getLevel(correctLevel));
return true;
}
return false;
}
/**
* Calculates the actually gained experience for every user in the given servers and adds them to the users.
* This method only actually increases the message count, and calls other methods for experience gain
* and role change.
* Loads the level and role configuration for each server and sorts them for them to be used.
* Only actually updates the role, if the user also changed level.
* @param servers The list of {@link AServer} containing the user which need to gain experience
*/
@Transactional
@Override
public void handleExperienceGain(List<AServer> servers) {
@@ -137,93 +148,133 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
Integer multiplier = configService.getDoubleValue("expMultiplier", serverExp.getId()).intValue();
PrimitiveIterator.OfInt iterator = new Random().ints(serverExp.getUsers().size(), minExp, maxExp + 1).iterator();
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRoleForServer(serverExp);
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(serverExp);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
serverExp.getUsers().forEach(userInAServer -> {
Integer gainedExperience = iterator.next();
gainedExperience *= multiplier;
log.trace("Handling {}. The user gains {}", userInAServer.getUserReference().getId(), gainedExperience);
AUserExperience userExperience = userExperienceManagementService.findUserInServer(userInAServer);
increaseExpForUser(userExperience, gainedExperience.longValue(), levels);
userExperience.setMessageCount(userExperience.getMessageCount() + 1);
handleExperienceRoleForUser(userExperience, roles);
AUserExperience aUserExperience = userExperienceManagementService.incrementExpForUser(userInAServer, gainedExperience.longValue(), 1L);
updateUserlevel(aUserExperience, levels);
updateUserRole(aUserExperience, roles);
userExperienceManagementService.saveUser(aUserExperience);
});
});
}
/**
* Calculates the appropriate level of the user and changes the role, if the {@link AExperienceLevel} changes.
* This changes the config in the database, and also gives the {@link net.dv8tion.jda.api.entities.Member} the new
* {@link net.dv8tion.jda.api.entities.Role}. If the user does not warrant an {@link AExperienceRole},
* this method also removes it. The role is only changed, if the user does not have
* @param userExperience The {@link AUserExperience} object to recalculate the {@link AExperienceRole} for
* @param roles The list of {@link AExperienceRole} used as a role configuration
*/
@Override
public void handleExperienceRoleForUser(AUserExperience userExperience, List<AExperienceRole> roles) {
public void updateUserRole(AUserExperience userExperience, List<AExperienceRole> roles) {
AUserInAServer user = userExperience.getUser();
log.trace("Updating experience role for user {} in server {}", user.getUserReference().getId(), user.getServerReference().getId());
AExperienceRole role = calculateRole(userExperience, roles);
AExperienceRole role = experienceRoleService.calculateRole(userExperience, roles);
Member member = botService.getMemberInServer(user.getServerReference(), user.getUserReference());
boolean currentlyHasNoExperienceRole = userExperience.getCurrentExperienceRole() == null;
if(role == null) {
if(!currentlyHasNoExperienceRole){
roleService.removeRoleFromUser(user, userExperience.getCurrentExperienceRole().getRole());
}
userExperience.setCurrentExperienceRole(null);
return;
}
if(currentlyHasNoExperienceRole || !role.getRole().getId().equals(userExperience.getCurrentExperienceRole().getRole().getId())) {
log.info("User {} in server {} gets a new role {}", user.getUserReference().getId(), user.getServerReference().getId(), role.getRole().getId());
if(!currentlyHasNoExperienceRole) {
roleService.removeRoleFromUser(user, userExperience.getCurrentExperienceRole().getRole());
boolean userHasRoleAlready = roleService.memberHasRole(member, role.getRole());
if(!userHasRoleAlready) {
if(currentlyHasNoExperienceRole || !role.getRole().getId().equals(userExperience.getCurrentExperienceRole().getRole().getId())) {
log.info("User {} in server {} gets a new role {}", user.getUserReference().getId(), user.getServerReference().getId(), role.getRole().getId());
if(!currentlyHasNoExperienceRole) {
roleService.removeRoleFromUser(user, userExperience.getCurrentExperienceRole().getRole());
}
roleService.addRoleToUser(user, role.getRole());
}
userExperience.setCurrentExperienceRole(role);
roleService.addRoleToUser(user, userExperience.getCurrentExperienceRole().getRole());
}
userExperience.setCurrentExperienceRole(role);
}
/**
* Synchronizes the {@link net.dv8tion.jda.api.entities.Role} of all {@link net.dv8tion.jda.api.entities.Member} in
* the given {@link AServer}. This might take a long time to complete, because there are a lot of role changes.
* @param server The {@link AServer} to update the users for
*/
@Override
public void syncUserRoles(AServer server) {
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRoleForServer(server);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
for (int i = 0; i < aUserExperiences.size(); i++) {
AUserExperience userExperience = aUserExperiences.get(i);
log.trace("Synchronizing {} out of {}", i, aUserExperiences.size());
handleExperienceRoleForUser(userExperience, roles);
updateUserRole(userExperience, roles);
}
}
/**
* Synchronizes the roles of all the users and provides feedback to the user executing
* @param server The {@link AServer} to update users for
* @param channel The {@link AChannel} in which the {@link dev.sheldan.abstracto.experience.models.templates.UserSyncStatusModel}
*/
@Override
public void syncUserRolesWithFeedback(AServer server, AChannel channel) {
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRoleForServer(server);
UserSyncStatusModel statusModel = UserSyncStatusModel.builder().currentCount(0).totalUserCount(aUserExperiences.size()).build();
MessageToSend status = templateService.renderEmbedTemplate("status_message", statusModel);
try {
Message statusMessage = messageService.createStatusMessage(status, channel).get();
int interval = Math.min(aUserExperiences.size() / 10, 100);
for (int i = 0; i < aUserExperiences.size(); i++) {
if((i % interval) == 1) {
log.trace("Updating feedback message with new index {} out of {}", i, aUserExperiences.size());
UserSyncStatusModel incrementalStatusModel = UserSyncStatusModel.builder().currentCount(i).totalUserCount(aUserExperiences.size()).build();
status = templateService.renderEmbedTemplate("status_message", incrementalStatusModel);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
}
log.trace("Synchronizing {} out of {}", i, aUserExperiences.size());
AUserExperience userExperience = aUserExperiences.get(i);
handleExperienceRoleForUser(userExperience, roles);
}
UserSyncStatusModel incrementalStatusModel = UserSyncStatusModel.builder().currentCount(aUserExperiences.size()).totalUserCount(aUserExperiences.size()).build();
status = templateService.renderEmbedTemplate("status_message", incrementalStatusModel);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
executeActionOnUserExperiencesWithFeedBack(aUserExperiences, channel, (AUserExperience experience) -> {
updateUserRole(experience, roles);
});
}
@Override
public void executeActionOnUserExperiencesWithFeedBack(List<AUserExperience> experiences, AChannel channel, Consumer<AUserExperience> toExecute) {
MessageToSend status = getUserSyncStatusUpdateModel(0, experiences.size());
try {
Message statusMessage = messageService.createStatusMessage(status, channel).get();
int interval = Math.min(Math.max(experiences.size() / 10, 1), 100);
for (int i = 0; i < experiences.size(); i++) {
if((i % interval) == 1) {
log.trace("Updating feedback message with new index {} out of {}", i, experiences.size());
status = getUserSyncStatusUpdateModel(i, experiences.size());
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
}
toExecute.accept(experiences.get(i));
log.trace("Synchronizing {} out of {}", i, experiences.size());
}
status = getUserSyncStatusUpdateModel(experiences.size(), experiences.size());
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
} catch (InterruptedException | ExecutionException e) {
log.info("Failed to synchronize users.", e);
}
}
private MessageToSend getUserSyncStatusUpdateModel(Integer current, Integer total) {
UserSyncStatusModel statusModel = UserSyncStatusModel.builder().currentCount(current).totalUserCount(total).build();
return templateService.renderEmbedTemplate("user_sync_status_message", statusModel);
}
/**
* Retrieves the role configuration and executes the method responsible to sync the experience role of the user
* @param userExperience The {@link AUserExperience} to synchronize the role for
*/
@Override
public void syncForSingleUser(AUserExperience userExperience) {
AUserInAServer user = userExperience.getUser();
log.info("Synchronizing for user {} in server {}", user.getUserReference().getId(), user.getServerReference().getId());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRoleForServer(user.getServerReference());
handleExperienceRoleForUser(userExperience, roles);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(user.getServerReference());
updateUserRole(userExperience, roles);
}
/**
* Retrieves the leaderboard data for the given page of the given server
* @param server The {@link AServer} to retrieve the leaderboard for
* @param page The desired page on the leaderboard. The pagesize is 10
* @return The {@link LeaderBoard} containing all necessary information concerning the leaderboard
*/
@Override
public LeaderBoard findLeaderBoardData(AServer server, Integer page) {
List<AUserExperience> experiences = userExperienceManagementService.findLeaderboardUsersPaginated(server, page * 10, (page +1) * 10);
@@ -235,6 +286,11 @@ public class ExperienceTrackerServiceBean implements ExperienceTrackerService {
return LeaderBoard.builder().entries(entries).build();
}
/**
* Builds an {@link AUserExperience} and loads the appropriate rank of the passed {@link AUserInAServer}
* @param userInAServer The {@link AUserInAServer} to retrieve the {@link LeaderBoardEntry} for
* @return The {@link LeaderBoardEntry} representing one single row in the leaderboard
*/
@Override
public LeaderBoardEntry getRankOfUserInServer(AUserInAServer userInAServer) {
log.info("Retrieving rank for {}", userInAServer.getUserReference().getId());

View File

@@ -13,16 +13,34 @@ public class ExperienceLevelServiceBean implements ExperienceLevelService {
@Autowired
private ExperienceLevelManagementService experienceLevelManagementService;
@Override
public void createExperienceLevel(Integer level, Long experienceNeeded) {
private void createExperienceLevel(Integer level, Long experienceNeeded) {
if(!experienceLevelManagementService.levelExists(level)) {
log.trace("Creating new experience level {} with experience needed {}.", level, experienceNeeded);
experienceLevelManagementService.createExperienceLevel(level, experienceNeeded);
}
}
/**
* Creates all {@link AExperienceLevel} until (including 0) up until the passed level
* @param level The max level to create {@link dev.sheldan.abstracto.experience.models.database.AExperienceLevel} for
*/
@Override
public Long calculateExperienceForLevel(Integer level) {
public void createLevelsUntil(Integer level) {
createExperienceLevel(0, 0L);
long experience = 0L;
for (int i = 1; i < level; i++) {
experience = experience + calculateExperienceForLevel(i - 1);
createExperienceLevel(i, experience);
}
}
/**
* Calculates the required experience to reach this level. This calculated experience is relative, in the sense
* the returned experience is the increment from the experience requirement from the level before.
* @param level The level to calculate the experience amount for
* @return The needed experience to reach this level, if the user already has the level below the passed one
*/
private Long calculateExperienceForLevel(Integer level) {
return 5L * (level * level) + 50 * level + 100;
}

View File

@@ -1,9 +1,11 @@
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.experience.models.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.models.database.AExperienceRole;
import dev.sheldan.abstracto.experience.models.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
@@ -11,7 +13,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
@@ -24,30 +28,79 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
private ExperienceLevelManagementService experienceLevelService;
@Autowired
private ExperienceTrackerService experienceTrackerService;
private AUserExperienceService userExperienceService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
/**
* 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 awareded at
* @param server The {@link AServer} for which this configuration should be done
*/
@Override
public void setRoleToLevel(ARole role, Integer level, AServer server) {
public void setRoleToLevel(ARole role, Integer level, AServer server, AChannel feedbackChannel) {
AExperienceLevel experienceLevel = experienceLevelService.getLevel(level);
experienceRoleManagementService.unSetLevelInServer(experienceLevel, server);
unsetRole(role, server, feedbackChannel);
experienceRoleManagementService.removeAllRoleAssignmentsForLevelInServer(experienceLevel, server);
experienceRoleManagementService.setLevelToRole(experienceLevel, role, server);
}
/**
* 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.models.database.AExperienceRole}
* configuration
* @param server The {@link AServer} for which the {@link ARole} should be removed from the configuration
*/
@Override
public void unsetRole(ARole role, AServer server) {
public void unsetRole(ARole role, AServer server, AChannel feedbackChannel) {
AExperienceRole roleInServer = experienceRoleManagementService.getRoleInServer(role, server);
if(roleInServer.getUsers().size() > 0) {
log.info("Recalculating the roles for {} users, because their current role was removed from experience tracking.", roleInServer.getUsers().size());
roleInServer.getUsers().forEach(userExperience -> {
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRoleForServer(server);
if(roleInServer != null) {
if(roleInServer.getUsers().size() > 0) {
log.info("Recalculating the roles for {} users, because their current role was removed from experience tracking.", roleInServer.getUsers().size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.removeIf(role1 -> role1.getId().equals(roleInServer.getId()));
experienceTrackerService.handleExperienceRoleForUser(userExperience, roles);
});
userExperienceService.executeActionOnUserExperiencesWithFeedBack(roleInServer.getUsers(), feedbackChannel, (AUserExperience ex) -> {
userExperienceService.updateUserRole(ex, roles);
});
}
experienceRoleManagementService.unsetRole(roleInServer);
}
experienceRoleManagementService.unsetRole(roleInServer);
}
/**
* Finds the best {@link AExperienceRole} for the level of the passed {@link AUserExperience}
* @param userExperience The {@link AUserExperience} containing the level to calculate the {@link AExperienceRole}
* @param roles The role configuration to be used when calculating the appropriate {@link AExperienceRole}
* @return The best fitting {@link AExperienceRole} according to the level of the {@link AUserExperience}
*/
@Override
public AExperienceRole calculateRole(AUserExperience userExperience, List<AExperienceRole> roles) {
if(roles.size() == 0) {
return null;
}
AExperienceRole lastRole = null;
for (AExperienceRole experienceRole : roles) {
if(userExperience.getCurrentLevel().getLevel() >= experienceRole.getLevel().getLevel()) {
lastRole = experienceRole;
} else {
return lastRole;
}
}
return lastRole;
}
@Override
public AExperienceLevel getLevelOfNextRole(AExperienceLevel startLevel, AServer server) {
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles = roles.stream().filter(role -> role.getLevel().getLevel() < startLevel.getLevel()).collect(Collectors.toList());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
AExperienceRole aExperienceRole = roles.stream().findFirst().orElse(null);
return aExperienceRole != null ? aExperienceRole.getLevel() : AExperienceLevel.builder().level(200).build();
}
}

View File

@@ -34,11 +34,6 @@ public class ExperienceLevelManagementServiceBean implements ExperienceLevelMana
return experienceLevelRepository.getOne(level);
}
@Override
public AExperienceLevel getLevelClosestTo(Long experience) {
return experienceLevelRepository.findTopByExperienceNeededGreaterThanOrderByExperienceNeededAsc(experience);
}
@Override
public List<AExperienceLevel> getLevelConfig() {
return experienceLevelRepository.findAll();

View File

@@ -18,8 +18,13 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
@Autowired
private ExperienceRoleRepository experienceRoleRepository;
/**
* Removes all assignments of roles for the given level
* @param level The level to remove the roles for
* @param server The server in which this should happen
*/
@Override
public void unSetLevelInServer(AExperienceLevel level, AServer server) {
public void removeAllRoleAssignmentsForLevelInServer(AExperienceLevel level, AServer server) {
log.trace("Removing all role assignments for level {}.", level.getLevel());
List<AExperienceRole> existingExperienceRoles = experienceRoleRepository.findByLevelAndRoleServer(level, server);
existingExperienceRoles.forEach(existingRole -> {
@@ -38,7 +43,7 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
}
@Override
public List<AExperienceRole> getExperienceRoleForServer(AServer server) {
public List<AExperienceRole> getExperienceRolesForServer(AServer server) {
return experienceRoleRepository.findByRoleServer(server);
}

View File

@@ -2,7 +2,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.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.models.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.models.database.AUserExperience;
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
@@ -23,12 +23,6 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
@Autowired
private ExperienceLevelManagementService experienceLevelManagementService;
@Override
public void setExperienceTo(AUserExperience aUserInAServer, Long experience) {
aUserInAServer.setExperience(experience);
repository.save(aUserInAServer);
}
@Override
public AUserExperience findUserInServer(AUserInAServer aUserInAServer) {
Optional<AUserExperience> byId = repository.findById(aUserInAServer.getUserInServerId());
@@ -38,7 +32,7 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
@Override
public AUserExperience createUserInServer(AUserInAServer aUserInAServer) {
AExperienceLevel startingLevel = experienceLevelManagementService.getLevel(0);
AUserExperience userExperience = AUserExperience
return AUserExperience
.builder()
.experience(0L)
.messageCount(0L)
@@ -46,13 +40,6 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
.id(aUserInAServer.getUserInServerId())
.currentLevel(startingLevel)
.build();
repository.save(userExperience);
return userExperience;
}
@Override
public void saveUser(AUserExperience userExperience) {
repository.save(userExperience);
}
@Override
@@ -60,6 +47,34 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
return repository.findByUser_ServerReference(server);
}
/**
* Creates or updates the {@link AUserExperience} object. Does not change the level or the role.
* @param user The {@link AUserInAServer} to increase the experience for
* @param experience The experience amount to increase by
* @param messageCount The amount of messags to increase the count by
* @return The created/changed {@link AUserExperience} object
*/
@Override
public AUserExperience incrementExpForUser(AUserInAServer user, Long experience, Long messageCount) {
Optional<AUserExperience> byId = repository.findById(user.getUserInServerId());
if(byId.isPresent()) {
AUserExperience userExperience = byId.get();
userExperience.setMessageCount(userExperience.getMessageCount() + messageCount);
userExperience.setExperience(userExperience.getExperience() + experience);
return userExperience;
} else {
AExperienceLevel startingLevel = experienceLevelManagementService.getLevel(0);
return AUserExperience
.builder()
.experience(experience)
.messageCount(messageCount)
.user(user)
.id(user.getUserInServerId())
.currentLevel(startingLevel)
.build();
}
}
@Override
public List<AUserExperience> findLeaderboardUsersPaginated(AServer aServer, Integer start, Integer end) {
return repository.findTop10ByUser_ServerReferenceOrderByExperienceDesc(aServer, PageRequest.of(start, end));
@@ -69,6 +84,11 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
public LeaderBoardEntryResult getRankOfUserInServer(AUserExperience userExperience) {
return repository.getRankOfUserInServer(userExperience.getId());
}
@Override
public void saveUser(AUserExperience userExperience) {
repository.save(userExperience);
}
}