store the users scoresaber profile and add more data to the top scores endpoint
This commit is contained in:
parent
0f7a890e44
commit
242a8a2fba
26
API/src/main/java/cc/fascinated/common/DateUtils.java
Normal file
26
API/src/main/java/cc/fascinated/common/DateUtils.java
Normal file
@ -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)));
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
166
API/src/main/java/cc/fascinated/common/TimeUtils.java
Normal file
166
API/src/main/java/cc/fascinated/common/TimeUtils.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* E.g: 1d, 1h, 1d1h, etc
|
||||
* </p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
*/
|
||||
|
@ -38,7 +38,7 @@ public class ScoresController {
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/top/{platform}")
|
||||
public ResponseEntity<List<?>> getTopScores(@PathVariable String platform) {
|
||||
return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 100));
|
||||
return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 50));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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.
|
||||
* </p>
|
||||
*/
|
||||
@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.
|
||||
*
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<User, UUID> {
|
||||
* @return the user
|
||||
*/
|
||||
Optional<User> findBySteamId(String steamId);
|
||||
|
||||
/**
|
||||
* Fetches all users and only their steam ids.
|
||||
*
|
||||
* @return the list of users
|
||||
*/
|
||||
@Query(value = "{}", fields = "{ 'steamId' : 1 }")
|
||||
List<User> fetchOnlySteamIds();
|
||||
|
||||
/**
|
||||
* Finds a user by their username.
|
||||
*
|
||||
* @param username the username of the user
|
||||
* @return the user
|
||||
*/
|
||||
@Query("{ $text: { $search: ?0 } }")
|
||||
List<User> findUsersByUsername(String username);
|
||||
}
|
||||
|
@ -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 {
|
||||
* </p>
|
||||
*/
|
||||
@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.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<ScoreSaberAccountToken> 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();
|
||||
}
|
||||
|
@ -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<TrackedScore> getTopScores(Platform.Platforms platform, int amount) {
|
||||
List<TrackedScore> scores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
|
||||
if (scores.isEmpty()) {
|
||||
public List<ScoreResponse> getTopScores(Platform.Platforms platform, int amount) {
|
||||
List<TrackedScore> foundScores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
|
||||
if (foundScores.isEmpty()) {
|
||||
throw new BadRequestException("No scores found for platform " + platform.getPlatformName());
|
||||
}
|
||||
|
||||
List<ScoreResponse> 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;
|
||||
}
|
||||
|
||||
|
@ -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<User> getUsers() {
|
||||
return (List<User>) this.userRepository.findAll();
|
||||
public List<User> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user