diff --git a/API/src/main/java/cc/fascinated/common/DateUtils.java b/API/src/main/java/cc/fascinated/common/DateUtils.java new file mode 100644 index 0000000..3775823 --- /dev/null +++ b/API/src/main/java/cc/fascinated/common/DateUtils.java @@ -0,0 +1,26 @@ +package cc.fascinated.common; + +import lombok.experimental.UtilityClass; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * @author Fascinated (fascinated7) + */ +@UtilityClass +public class DateUtils { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT; + + /** + * Gets the date from a string. + * + * @param date The date string. + * @return The date. + */ + public static Date getDateFromString(String date) { + return Date.from(Instant.from(FORMATTER.parse(date))); + } +} diff --git a/API/src/main/java/cc/fascinated/common/MathUtils.java b/API/src/main/java/cc/fascinated/common/MathUtils.java index 42b508a..06a5b03 100644 --- a/API/src/main/java/cc/fascinated/common/MathUtils.java +++ b/API/src/main/java/cc/fascinated/common/MathUtils.java @@ -2,6 +2,9 @@ package cc.fascinated.common; import lombok.experimental.UtilityClass; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; + /** * @author Fascinated (fascinated7) */ @@ -31,4 +34,19 @@ public class MathUtils { public static double lerp(double a, double b, double t) { return a + t * (b - a); } + + /** + * Format a number to a specific amount of decimal places. + * + * @param number the number to format + * @param additional the additional decimal places to format + * @return the formatted number + */ + public static double format(double number, int additional) { + return Double.parseDouble( + new DecimalFormat("#.#" + "#".repeat(Math.max(0, additional - 1)), + new DecimalFormatSymbols() + ).format(number) + ); + } } diff --git a/API/src/main/java/cc/fascinated/common/TimeUtils.java b/API/src/main/java/cc/fascinated/common/TimeUtils.java new file mode 100644 index 0000000..b9664e5 --- /dev/null +++ b/API/src/main/java/cc/fascinated/common/TimeUtils.java @@ -0,0 +1,166 @@ +package cc.fascinated.common; + +import lombok.*; +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Fascinated (fascinated7) + */ +@UtilityClass +public final class TimeUtils { + /** + * Format a time in millis to a readable time format. + * + * @param millis the millis to format + * @return the formatted time + */ + public static String format(long millis) { + return format(millis, BatTimeFormat.FIT); + } + + /** + * Format a time in millis to a readable time format. + * + * @param millis the millis to format + * @param timeUnit the time unit to format the millis to + * @return the formatted time + */ + public static String format(long millis, BatTimeFormat timeUnit) { + return format(millis, timeUnit, false); + } + + /** + * Format a time in millis to a readable time format. + * + * @param millis the millis to format + * @param timeUnit the time unit to format the millis to + * @param compact whether to use a compact display + * @return the formatted time + */ + public static String format(long millis, BatTimeFormat timeUnit, boolean compact) { + return format(millis, timeUnit, true, compact); + } + + /** + * Format a time in millis to a readable time format. + * + * @param millis the millis to format + * @param timeUnit the time unit to format the millis to + * @param decimals whether to include decimals + * @param compact whether to use a compact display + * @return the formatted time + */ + public static String format(long millis, BatTimeFormat timeUnit, boolean decimals, boolean compact) { + if (millis == -1L) { // Format permanent + return "Perm" + (compact ? "" : "anent"); + } + // Format the time to the best fitting time unit + if (timeUnit == BatTimeFormat.FIT) { + for (BatTimeFormat otherTimeUnit : BatTimeFormat.VALUES) { + if (otherTimeUnit != BatTimeFormat.FIT && millis >= otherTimeUnit.getMillis()) { + timeUnit = otherTimeUnit; + break; + } + } + } + double time = MathUtils.format((double) millis / timeUnit.getMillis(), 1); // Format the time + if (!decimals) { // Remove decimals + time = (int) time; + } + String formatted = time + (compact ? timeUnit.getSuffix() : " " + timeUnit.getDisplay()); // Append the time unit + if (time != 1.0 && !compact) { // Pluralize the time unit + formatted += "s"; + } + return formatted; + } + + /** + * Convert the given input into a time in millis. + *

+ * E.g: 1d, 1h, 1d1h, etc + *

+ * + * @param input the input to parse + * @return the time in millis + */ + public static long fromString(String input) { + Matcher matcher = BatTimeFormat.SUFFIX_PATTERN.matcher(input); // Match the given input + long millis = 0; // The total millis + + // Match corresponding suffixes and add up the total millis + while (matcher.find()) { + int amount = Integer.parseInt(matcher.group(1)); // The amount of time to add + String suffix = matcher.group(2); // The unit suffix + BatTimeFormat timeUnit = BatTimeFormat.fromSuffix(suffix); // The time unit to add + if (timeUnit != null) { // Increment the total millis + millis += amount * timeUnit.getMillis(); + } + } + return millis; + } + + /** + * Represents a unit of time. + */ + @NoArgsConstructor + @AllArgsConstructor + @Getter(AccessLevel.PRIVATE) + @ToString + public enum BatTimeFormat { + FIT, + YEARS("Year", "y", TimeUnit.DAYS.toMillis(365L)), + MONTHS("Month", "mo", TimeUnit.DAYS.toMillis(30L)), + WEEKS("Week", "w", TimeUnit.DAYS.toMillis(7L)), + DAYS("Day", "d", TimeUnit.DAYS.toMillis(1L)), + HOURS("Hour", "h", TimeUnit.HOURS.toMillis(1L)), + MINUTES("Minute", "m", TimeUnit.MINUTES.toMillis(1L)), + SECONDS("Second", "s", TimeUnit.SECONDS.toMillis(1L)), + MILLISECONDS("Millisecond", "ms", 1L); + + /** + * Our cached unit values. + */ + public static final BatTimeFormat[] VALUES = values(); + + /** + * Our cached suffix pattern. + */ + public static final Pattern SUFFIX_PATTERN = Pattern.compile("(\\d+)(mo|ms|[ywdhms])"); + + /** + * The display of this time unit. + */ + private String display; + + /** + * The suffix of this time unit. + */ + private String suffix; + + /** + * The amount of millis in this time unit. + */ + private long millis; + + /** + * Get the time unit with the given suffix. + * + * @param suffix the time unit suffix + * @return the time unit, null if not found + */ + @Nullable + public static BatTimeFormat fromSuffix(String suffix) { + for (BatTimeFormat unit : VALUES) { + if (unit != FIT && unit.getSuffix().equals(suffix)) { + return unit; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/API/src/main/java/cc/fascinated/controller/RootController.java b/API/src/main/java/cc/fascinated/controller/RootController.java index 7fccbda..027c574 100644 --- a/API/src/main/java/cc/fascinated/controller/RootController.java +++ b/API/src/main/java/cc/fascinated/controller/RootController.java @@ -1,13 +1,13 @@ package cc.fascinated.controller; -import java.util.Map; - import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import java.util.Map; + /** * @author Fascinated (fascinated7) */ diff --git a/API/src/main/java/cc/fascinated/controller/ScoresController.java b/API/src/main/java/cc/fascinated/controller/ScoresController.java index ae7875c..bcbc6ae 100644 --- a/API/src/main/java/cc/fascinated/controller/ScoresController.java +++ b/API/src/main/java/cc/fascinated/controller/ScoresController.java @@ -38,7 +38,7 @@ public class ScoresController { @ResponseBody @GetMapping(value = "/top/{platform}") public ResponseEntity> getTopScores(@PathVariable String platform) { - return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 100)); + return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 50)); } /** diff --git a/API/src/main/java/cc/fascinated/controller/UserController.java b/API/src/main/java/cc/fascinated/controller/UserController.java index e2f9f5b..53e8ce6 100644 --- a/API/src/main/java/cc/fascinated/controller/UserController.java +++ b/API/src/main/java/cc/fascinated/controller/UserController.java @@ -1,7 +1,6 @@ package cc.fascinated.controller; import cc.fascinated.exception.impl.BadRequestException; -import cc.fascinated.model.user.User; import cc.fascinated.model.user.UserDTO; import cc.fascinated.services.UserService; import lombok.NonNull; diff --git a/API/src/main/java/cc/fascinated/model/leaderboard/Difficulty.java b/API/src/main/java/cc/fascinated/model/leaderboard/Difficulty.java new file mode 100644 index 0000000..f9fb29c --- /dev/null +++ b/API/src/main/java/cc/fascinated/model/leaderboard/Difficulty.java @@ -0,0 +1,21 @@ +package cc.fascinated.model.leaderboard; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Fascinated (fascinated7) + */ +@AllArgsConstructor +@Getter +public class Difficulty { + /** + * The difficulty of the song. + */ + private String difficulty; + + /** + * The raw difficulty of the song. + */ + private String difficultyRaw; +} diff --git a/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java b/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java new file mode 100644 index 0000000..505ed26 --- /dev/null +++ b/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java @@ -0,0 +1,81 @@ +package cc.fascinated.model.leaderboard; + +import cc.fascinated.common.ScoreSaberUtils; +import cc.fascinated.model.token.ScoreSaberLeaderboardToken; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Fascinated (fascinated7) + */ +@AllArgsConstructor +@Getter +public class Leaderboard { + /** + * The ID of the leaderboard. + */ + private String id; + + /** + * The hash of the song. + */ + private String songHash; + + /** + * The name of the song. + */ + private String songName; + + /** + * The sub name of the song. + */ + private String songSubName; + + /** + * The author of the song. + */ + private String songAuthorName; + + /** + * The mapper of the song. + */ + private String levelAuthorName; + + /** + * The difficulty of the song. + */ + private Difficulty difficulty; + + /** + * The star rating for this leaderboard. + */ + private double stars; + /** + * The cover image for this leaderboard. + */ + private String coverImage; + + /** + * Constructs a new {@link Leaderboard} object + * from a {@link ScoreSaberLeaderboardToken} object. + * + * @param token The token to construct the object from. + * @return The leaderboard. + */ + public static Leaderboard getFromScoreSaberToken(ScoreSaberLeaderboardToken token) { + return new Leaderboard( + token.getId(), + token.getSongHash(), + token.getSongName(), + token.getSongSubName(), + token.getSongAuthorName(), + token.getLevelAuthorName(), + new Difficulty( + ScoreSaberUtils.parseDifficulty(token.getDifficulty().getDifficulty()), + token.getDifficulty().getDifficultyRaw() + ), + token.getStars(), + token.getCoverImage() + ); + } +} diff --git a/API/src/main/java/cc/fascinated/model/score/ScoreResponse.java b/API/src/main/java/cc/fascinated/model/score/ScoreResponse.java new file mode 100644 index 0000000..e2215ac --- /dev/null +++ b/API/src/main/java/cc/fascinated/model/score/ScoreResponse.java @@ -0,0 +1,95 @@ +package cc.fascinated.model.score; + +import cc.fascinated.model.leaderboard.Leaderboard; +import cc.fascinated.model.user.ScoreSaberAccount; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Date; + +/** + * @author Fascinated (fascinated7) + */ +@AllArgsConstructor +@Getter +public class ScoreResponse { + /** + * The ID of the score. + */ + private String scoreId; + + /** + * The ID of the player who set the score. + */ + private ScoreSaberAccount player; + + /** + * The ID of the leaderboard. + */ + private Leaderboard leaderboard; + + /** + * The PP of the score. + */ + private Double pp; + + /** + * The rank of the score. + */ + private Long rank; + + /** + * The base score of the score. + */ + private Long score; + + /** + * The modified score of the score. + */ + private Long modifiedScore; + + /** + * The weight of the score. + */ + private Double weight; + + /** + * The modifiers of the score. + */ + private String modifiers; + + /** + * The multiplier of the score. + */ + private double multiplier; + + /** + * The number of misses in the score. + */ + private Long missedNotes; + + /** + * The number of bad cuts in the score. + */ + private Long badCuts; + + /** + * The highest combo in the score. + */ + private Long maxCombo; + + /** + * The accuracy of the score. + */ + private Double accuracy; + + /** + * The difficulty the score was set on. + */ + private String difficulty; + + /** + * The timestamp of the score. + */ + private Date timestamp; +} diff --git a/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java b/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java new file mode 100644 index 0000000..86c77c6 --- /dev/null +++ b/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java @@ -0,0 +1,63 @@ +package cc.fascinated.model.user; + +import cc.fascinated.common.DateUtils; +import cc.fascinated.model.token.ScoreSaberAccountToken; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Date; + +/** + * @author Fascinated (fascinated7) + */ +@AllArgsConstructor +@Getter +public class ScoreSaberAccount { + /** + * The avatar of the user. + */ + private String avatar; + + /** + * The country of the user. + */ + private String country; + + /** + * The rank of the user. + */ + private int rank; + + /** + * The country rank of the user. + */ + private int countryRank; + + /** + * The date the user joined ScoreSaber. + */ + private Date accountCreated; + + /** + * The date the user was last updated. + */ + private Date lastUpdated; + + /** + * Constructs a new {@link ScoreSaberAccount} object + * from a {@link ScoreSaberAccountToken} object. + * + * @param token The token to construct the object from. + * @return The scoresaber account. + */ + public static ScoreSaberAccount getFromToken(ScoreSaberAccountToken token) { + return new ScoreSaberAccount( + token.getProfilePicture(), + token.getCountry(), + token.getRank(), + token.getCountryRank(), + DateUtils.getDateFromString(token.getFirstSeen()), + new Date() + ); + } +} diff --git a/API/src/main/java/cc/fascinated/model/user/User.java b/API/src/main/java/cc/fascinated/model/user/User.java index 192d062..6cdda06 100644 --- a/API/src/main/java/cc/fascinated/model/user/User.java +++ b/API/src/main/java/cc/fascinated/model/user/User.java @@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; import java.util.UUID; @@ -15,6 +17,7 @@ import java.util.UUID; @Getter @Setter @ToString +@Document("user") public class User { /** * The ID of the user. @@ -28,11 +31,13 @@ public class User { * Usually their Steam name. *

*/ + @Indexed private String username; /** * The ID of the users steam profile. */ + @Indexed(unique = true) private String steamId; /** @@ -44,6 +49,11 @@ public class User { */ public boolean hasLoggedIn; + /** + * The user's ScoreSaber account token. + */ + public ScoreSaberAccount scoresaberAccount; + /** * Converts the User object to a UserDTO object. * diff --git a/API/src/main/java/cc/fascinated/platform/Platform.java b/API/src/main/java/cc/fascinated/platform/Platform.java index 895a7da..ce54c28 100644 --- a/API/src/main/java/cc/fascinated/platform/Platform.java +++ b/API/src/main/java/cc/fascinated/platform/Platform.java @@ -69,20 +69,19 @@ public abstract class Platform { public abstract double getPp(double stars, double accuracy); /** - * Called every 10 minutes to update - * the players data in QuestDB. + * Called to update the players + * data in QuestDB. */ public abstract void updatePlayers(); /** - * Called every 10 minutes to update - * the metrics for total scores, etc. + * Called to update the metrics + * for total scores, etc. */ public abstract void updateMetrics(); /** - * Called every day at midnight to update - * the leaderboards. + * Called to update the leaderboards. */ public abstract void updateLeaderboards(); diff --git a/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java b/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java index 250222d..935d4df 100644 --- a/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java +++ b/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java @@ -19,7 +19,6 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -153,7 +152,7 @@ public class ScoreSaberPlatform extends Platform { @Override public void updatePlayers() { - for (User user : this.userService.getUsers()) { + for (User user : this.userService.getUsers(false)) { if (!user.isHasLoggedIn()) { // Check if the user has linked their account continue; } diff --git a/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java b/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java index 2f44b79..edacae4 100644 --- a/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java +++ b/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java @@ -2,7 +2,9 @@ package cc.fascinated.repository.mongo; import cc.fascinated.model.user.User; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -17,4 +19,21 @@ public interface UserRepository extends MongoRepository { * @return the user */ Optional findBySteamId(String steamId); + + /** + * Fetches all users and only their steam ids. + * + * @return the list of users + */ + @Query(value = "{}", fields = "{ 'steamId' : 1 }") + List fetchOnlySteamIds(); + + /** + * Finds a user by their username. + * + * @param username the username of the user + * @return the user + */ + @Query("{ $text: { $search: ?0 } }") + List findUsersByUsername(String username); } diff --git a/API/src/main/java/cc/fascinated/services/PlatformService.java b/API/src/main/java/cc/fascinated/services/PlatformService.java index 0f03d0b..965acb5 100644 --- a/API/src/main/java/cc/fascinated/services/PlatformService.java +++ b/API/src/main/java/cc/fascinated/services/PlatformService.java @@ -2,7 +2,6 @@ package cc.fascinated.services; import cc.fascinated.platform.Platform; import cc.fascinated.platform.impl.ScoreSaberPlatform; -import cc.fascinated.repository.couchdb.TrackedScoreRepository; import com.mongodb.client.model.Filters; import lombok.NonNull; import lombok.extern.log4j.Log4j2; @@ -59,12 +58,12 @@ public class PlatformService { *

*/ @Scheduled(cron = "0 */15 * * * *") - public void updateScores() { - log.info("Updating %s platform players...".formatted(this.platforms.size())); + public void updatePlayerMetrics() { + log.info("Updating %s platform player metrics...".formatted(this.platforms.size())); for (Platform platform : this.platforms) { platform.updatePlayers(); } - log.info("Finished updating platform players."); + log.info("Finished updating platform player metrics."); } /** diff --git a/API/src/main/java/cc/fascinated/services/ScoreSaberService.java b/API/src/main/java/cc/fascinated/services/ScoreSaberService.java index 77e9033..c28a875 100644 --- a/API/src/main/java/cc/fascinated/services/ScoreSaberService.java +++ b/API/src/main/java/cc/fascinated/services/ScoreSaberService.java @@ -10,8 +10,6 @@ import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository; import kong.unirest.core.HttpResponse; import lombok.NonNull; import lombok.extern.log4j.Log4j2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -50,15 +48,15 @@ public class ScoreSaberService { */ public ScoreSaberAccountToken getAccount(User user) { if (user.getSteamId() == null) { - throw new BadRequestException("%s does not have a linked ScoreSaber account".formatted(user.getUsername())); + throw new BadRequestException("The user does not have a steam id"); } HttpResponse response = Request.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()), ScoreSaberAccountToken.class); if (response.getParsingError().isPresent()) { // Failed to parse the response - throw new BadRequestException("Failed to parse ScoreSaber account for '%s'".formatted(user.getUsername())); + throw new BadRequestException("Failed to parse ScoreSaber account for '%s'".formatted(user.getSteamId())); } if (response.getStatus() != 200) { // The response was not successful - throw new BadRequestException("Failed to get ScoreSaber account for '%s'".formatted(user.getUsername())); + throw new BadRequestException("Failed to get ScoreSaber account for '%s'".formatted(user.getSteamId())); } return response.getBody(); } diff --git a/API/src/main/java/cc/fascinated/services/TrackedScoreService.java b/API/src/main/java/cc/fascinated/services/TrackedScoreService.java index 3afca8f..36ad026 100644 --- a/API/src/main/java/cc/fascinated/services/TrackedScoreService.java +++ b/API/src/main/java/cc/fascinated/services/TrackedScoreService.java @@ -1,15 +1,19 @@ package cc.fascinated.services; import cc.fascinated.exception.impl.BadRequestException; +import cc.fascinated.model.leaderboard.Leaderboard; +import cc.fascinated.model.score.ScoreResponse; import cc.fascinated.model.score.ScoresOverResponse; import cc.fascinated.model.score.TotalScoresResponse; import cc.fascinated.model.score.TrackedScore; +import cc.fascinated.model.user.User; import cc.fascinated.platform.Platform; import cc.fascinated.repository.couchdb.TrackedScoreRepository; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.LinkedList; import java.util.List; /** @@ -28,9 +32,24 @@ public class TrackedScoreService { @NonNull private final TrackedScoreRepository trackedScoreRepository; + /** + * The user service to use. + */ + @NonNull + private final UserService userService; + + /** + * The ScoreSaber service to use. + */ + @NonNull + private final ScoreSaberService scoreSaberService; + @Autowired - public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) { + public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository, @NonNull UserService userService, + @NonNull ScoreSaberService scoreSaberService) { this.trackedScoreRepository = trackedScoreRepository; + this.userService = userService; + this.scoreSaberService = scoreSaberService; this.trackedScoreRepository.ensureDeduplication(); } @@ -42,11 +61,41 @@ public class TrackedScoreService { * @param amount the amount of scores to get * @return the scores */ - public List getTopScores(Platform.Platforms platform, int amount) { - List scores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount); - if (scores.isEmpty()) { + public List getTopScores(Platform.Platforms platform, int amount) { + List foundScores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount); + if (foundScores.isEmpty()) { throw new BadRequestException("No scores found for platform " + platform.getPlatformName()); } + + List scores = new LinkedList<>(); + for (TrackedScore trackedScore : foundScores) { + User user = this.userService.getUser(trackedScore.getPlayerId()); + Leaderboard leaderboard = null; + switch (platform) { + case SCORESABER -> leaderboard = Leaderboard.getFromScoreSaberToken(this.scoreSaberService.getLeaderboard(trackedScore.getLeaderboardId())); + } + assert leaderboard != null; // This should never be null + + scores.add(new ScoreResponse( + trackedScore.getScoreId(), + user.getScoresaberAccount(), + leaderboard, + trackedScore.getPp(), + trackedScore.getRank(), + trackedScore.getScore(), + trackedScore.getModifiedScore(), + trackedScore.getWeight(), + trackedScore.getModifiers(), + trackedScore.getMultiplier(), + trackedScore.getMissedNotes(), + trackedScore.getBadCuts(), + trackedScore.getMaxCombo(), + trackedScore.getAccuracy(), + trackedScore.getDifficulty(), + trackedScore.getTimestamp() + )); + } + return scores; } diff --git a/API/src/main/java/cc/fascinated/services/UserService.java b/API/src/main/java/cc/fascinated/services/UserService.java index 41fd189..0172121 100644 --- a/API/src/main/java/cc/fascinated/services/UserService.java +++ b/API/src/main/java/cc/fascinated/services/UserService.java @@ -34,7 +34,7 @@ public class UserService { * @throws BadRequestException if the user is not found */ public User getUser(String steamId) { - if (!this.validateSteamId(steamId)) { + if (!this.isValidSteamId(steamId)) { throw new BadRequestException("Invalid steam id"); } @@ -49,13 +49,12 @@ public class UserService { } /** - * Creates a user in the database + * Saves a user to the database * - * @param user the user to create - * @return the created user + * @param user the user to save */ - public User createUser(User user) { - return this.userRepository.save(user); + public void saveUser(User user) { + this.userRepository.save(user); } /** @@ -63,8 +62,11 @@ public class UserService { * * @return all users */ - public List getUsers() { - return (List) this.userRepository.findAll(); + public List getUsers(boolean steamIdsOnly) { + if (steamIdsOnly) { + return this.userRepository.fetchOnlySteamIds(); + } + return this.userRepository.findAll(); } /** @@ -73,7 +75,7 @@ public class UserService { * @param steamId the steam id to validate * @return if the steam id is valid */ - public boolean validateSteamId(String steamId) { + public boolean isValidSteamId(String steamId) { return steamId != null && steamId.length() == 17; } } diff --git a/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java b/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java index 12e64ed..b8d774b 100644 --- a/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java +++ b/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java @@ -1,14 +1,14 @@ package cc.fascinated.websocket.impl; import cc.fascinated.common.ScoreSaberUtils; -import cc.fascinated.model.token.ScoreSaberLeaderboardToken; -import cc.fascinated.model.token.ScoreSaberPlayerScoreToken; -import cc.fascinated.model.token.ScoreSaberScoreToken; -import cc.fascinated.model.token.ScoreSaberWebsocketDataToken; +import cc.fascinated.common.TimeUtils; +import cc.fascinated.model.token.*; +import cc.fascinated.model.user.ScoreSaberAccount; +import cc.fascinated.model.user.User; import cc.fascinated.platform.Platform; -import cc.fascinated.platform.impl.ScoreSaberPlatform; import cc.fascinated.services.PlatformService; import cc.fascinated.services.QuestDBService; +import cc.fascinated.services.ScoreSaberService; import cc.fascinated.services.UserService; import cc.fascinated.websocket.Websocket; import com.fasterxml.jackson.databind.ObjectMapper; @@ -20,12 +20,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.TextMessage; +import java.util.Date; +import java.util.concurrent.TimeUnit; + /** * @author Fascinated (fascinated7) */ @Component @Log4j2(topic = "ScoreSaber Websocket") public class ScoreSaberWebsocket extends Websocket { + /** + * The interval to force update the user's account. + */ + private static long FORCE_UPDATE_INTERVAL = TimeUnit.HOURS.toMillis(12); + /** * The Jackson deserializer to use. */ @@ -46,17 +54,24 @@ public class ScoreSaberWebsocket extends Websocket { */ private final PlatformService platformService; + /** + * The ScoreSaber service to use + */ + private final ScoreSaberService scoreSaberService; + @Autowired public ScoreSaberWebsocket(@NonNull ObjectMapper objectMapper, @NonNull UserService userService, @NonNull QuestDBService questDBService, - @NonNull PlatformService platformService) { + @NonNull PlatformService platformService, @NonNull ScoreSaberService scoreSaberService) { super("ScoreSaber", "wss://scoresaber.com/ws"); this.objectMapper = objectMapper; this.userService = userService; this.questDBService = questDBService; this.platformService = platformService; + this.scoreSaberService = scoreSaberService; } - @Override @SneakyThrows + @Override + @SneakyThrows public void handleMessage(@NonNull TextMessage message) { String payload = message.getPayload(); if (payload.equals("Connected to the ScoreSaber WSS")) { // Ignore the connection message @@ -74,10 +89,24 @@ public class ScoreSaberWebsocket extends Websocket { ScoreSaberLeaderboardToken leaderboard = scoreToken.getLeaderboard(); ScoreSaberScoreToken.LeaderboardPlayerInfo player = score.getLeaderboardPlayerInfo(); - if (!userService.validateSteamId(player.getId())) { // Validate the Steam ID + // Ensure the player is valid + if (!this.userService.isValidSteamId(player.getId())) { return; } + User user = userService.getUser(player.getId()); + ScoreSaberAccount scoresaberAccount = user.getScoresaberAccount(); + // Ensure the users account is up-to-date + if (scoresaberAccount == null || scoresaberAccount.getLastUpdated().before(new Date(System.currentTimeMillis() - FORCE_UPDATE_INTERVAL))) { + log.info("Updating account for '{}', last update: {}", + player.getName(), + scoresaberAccount == null ? "now" : TimeUtils.format(System.currentTimeMillis() - scoresaberAccount.getLastUpdated().getTime()) + ); + ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(user); + user.setScoresaberAccount(ScoreSaberAccount.getFromToken(accountToken)); + userService.saveUser(user); // Save the user + } + double accuracy = ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100; String difficulty = ScoreSaberUtils.parseDifficulty(leaderboard.getDifficulty().getDifficulty()); double pp = platformService.getScoreSaberPlatform().getPp(leaderboard.getStars(), accuracy); // Recalculate the PP