From 242a8a2fbad6e5bc424a26d56f95b692feb93d35 Mon Sep 17 00:00:00 2001
From: Liam
Date: Thu, 1 Aug 2024 16:19:27 +0100
Subject: [PATCH] store the users scoresaber profile and add more data to the
top scores endpoint
---
.../java/cc/fascinated/common/DateUtils.java | 26 +++
.../java/cc/fascinated/common/MathUtils.java | 18 ++
.../java/cc/fascinated/common/TimeUtils.java | 166 ++++++++++++++++++
.../fascinated/controller/RootController.java | 4 +-
.../controller/ScoresController.java | 2 +-
.../fascinated/controller/UserController.java | 1 -
.../model/leaderboard/Difficulty.java | 21 +++
.../model/leaderboard/Leaderboard.java | 81 +++++++++
.../fascinated/model/score/ScoreResponse.java | 95 ++++++++++
.../model/user/ScoreSaberAccount.java | 63 +++++++
.../java/cc/fascinated/model/user/User.java | 10 ++
.../java/cc/fascinated/platform/Platform.java | 11 +-
.../platform/impl/ScoreSaberPlatform.java | 3 +-
.../repository/mongo/UserRepository.java | 19 ++
.../fascinated/services/PlatformService.java | 7 +-
.../services/ScoreSaberService.java | 8 +-
.../services/TrackedScoreService.java | 57 +++++-
.../cc/fascinated/services/UserService.java | 20 ++-
.../websocket/impl/ScoreSaberWebsocket.java | 45 ++++-
19 files changed, 615 insertions(+), 42 deletions(-)
create mode 100644 API/src/main/java/cc/fascinated/common/DateUtils.java
create mode 100644 API/src/main/java/cc/fascinated/common/TimeUtils.java
create mode 100644 API/src/main/java/cc/fascinated/model/leaderboard/Difficulty.java
create mode 100644 API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java
create mode 100644 API/src/main/java/cc/fascinated/model/score/ScoreResponse.java
create mode 100644 API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java
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