bob
This commit is contained in:
commit
7262188add
0
.gitea/workflows/ci.yml
Normal file
0
.gitea/workflows/ci.yml
Normal file
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
*.class
|
||||
*.log
|
||||
*.ctxt
|
||||
.mtj.tmp/
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
.idea
|
||||
cmake-build-*/
|
||||
.idea/**/mongoSettings.xml
|
||||
*.iws
|
||||
out/
|
||||
build/
|
||||
work/
|
||||
target/
|
||||
.idea_modules/
|
||||
atlassian-ide-plugin.xml
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
git.properties
|
||||
pom.xml.versionsBackup
|
||||
/docker/questdb/
|
||||
/docker/mongodb/
|
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
7
.idea/encodings.xml
generated
Normal file
7
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
14
.idea/misc.xml
generated
Normal file
14
.idea/misc.xml
generated
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/docker" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
17
docker/docker-compose.yml
Normal file
17
docker/docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
container_name: mongodb
|
||||
ports:
|
||||
- "27000:27017"
|
||||
volumes:
|
||||
- ./mongodb:/data/db
|
||||
questdb:
|
||||
image: questdb/questdb:8.0.3
|
||||
ports:
|
||||
- "8812:8812"
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- "./questdb:/var/lib/questdb"
|
118
pom.xml
Normal file
118
pom.xml
Normal file
@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cc.fascinated</groupId>
|
||||
<artifactId>YetAnotherBeatSaberTracker</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/com.konghq/unirest-java-bom -->
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-java-bom</artifactId>
|
||||
<version>4.4.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-java-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.konghq</groupId>
|
||||
<artifactId>unirest-modules-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.questdb</groupId>
|
||||
<artifactId>questdb</artifactId>
|
||||
<version>8.0.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Libraries -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>24.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
43
src/main/java/cc/fascinated/Main.java
Normal file
43
src/main/java/cc/fascinated/Main.java
Normal file
@ -0,0 +1,43 @@
|
||||
package cc.fascinated;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@EnableJpaRepositories(basePackages = "cc.fascinated.repository.couchdb")
|
||||
@EnableMongoRepositories(basePackages = "cc.fascinated.repository.mongo")
|
||||
@EnableScheduling
|
||||
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
|
||||
@Log4j2(topic = "Ember")
|
||||
public class Main {
|
||||
@SneakyThrows
|
||||
public static void main(@NonNull String[] args) {
|
||||
// Handle loading of our configuration file
|
||||
File config = new File("application.yml");
|
||||
if (!config.exists()) { // Saving the default config if it doesn't exist locally
|
||||
Files.copy(Objects.requireNonNull(Main.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved
|
||||
config.getAbsolutePath()
|
||||
);
|
||||
return;
|
||||
}
|
||||
log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
|
||||
|
||||
// Start the app
|
||||
SpringApplication.run(Main.class, args);
|
||||
}
|
||||
}
|
23
src/main/java/cc/fascinated/common/ScoreSaberUtils.java
Normal file
23
src/main/java/cc/fascinated/common/ScoreSaberUtils.java
Normal file
@ -0,0 +1,23 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public class ScoreSaberUtils {
|
||||
/**
|
||||
* Parses the difficulty of a map.
|
||||
*
|
||||
* @param difficulty the difficulty of the map
|
||||
* @return the parsed difficulty
|
||||
*/
|
||||
public static String parseDifficulty(int difficulty) {
|
||||
return switch (difficulty) {
|
||||
case 1 -> "Easy";
|
||||
case 3 -> "Normal";
|
||||
case 5 -> "Hard";
|
||||
case 7 -> "Expert";
|
||||
case 9 -> "Expert+";
|
||||
default -> "Unknown";
|
||||
};
|
||||
}
|
||||
}
|
25
src/main/java/cc/fascinated/common/UUIDUtils.java
Normal file
25
src/main/java/cc/fascinated/common/UUIDUtils.java
Normal file
@ -0,0 +1,25 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public class UUIDUtils {
|
||||
/**
|
||||
* Parses a UUID from a string.
|
||||
*
|
||||
* @param id the string to parse
|
||||
* @return the UUID
|
||||
* @throws BadRequestException if the UUID is invalid
|
||||
*/
|
||||
public static UUID parseUUID(String id) {
|
||||
try {
|
||||
return UUID.fromString(id);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BadRequestException("Invalid UUID");
|
||||
}
|
||||
}
|
||||
}
|
33
src/main/java/cc/fascinated/config/Config.java
Normal file
33
src/main/java/cc/fascinated/config/Config.java
Normal file
@ -0,0 +1,33 @@
|
||||
package cc.fascinated.config;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Configuration
|
||||
public class Config {
|
||||
|
||||
/**
|
||||
* Allow all origins to access the API
|
||||
*
|
||||
* @return the WebMvcConfigurer
|
||||
*/
|
||||
@Bean
|
||||
public WebMvcConfigurer configureCors() {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||
// Allow all origins to access the API
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*") // Allow all origins
|
||||
.allowedMethods("*") // Allow all methods
|
||||
.allowedHeaders("*"); // Allow all headers
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
58
src/main/java/cc/fascinated/controller/ScoresController.java
Normal file
58
src/main/java/cc/fascinated/controller/ScoresController.java
Normal file
@ -0,0 +1,58 @@
|
||||
package cc.fascinated.controller;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.score.TrackedScoreDTO;
|
||||
import cc.fascinated.platform.Platform;
|
||||
import cc.fascinated.services.TrackedScoreService;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/scores")
|
||||
public class ScoresController {
|
||||
/**
|
||||
* The tracked score service to use.
|
||||
*/
|
||||
@NonNull
|
||||
private final TrackedScoreService trackedScoreService;
|
||||
|
||||
@Autowired
|
||||
public ScoresController(@NonNull TrackedScoreService trackedScoreService) {
|
||||
this.trackedScoreService = trackedScoreService;
|
||||
}
|
||||
|
||||
/**
|
||||
* A GET mapping to retrieve the top
|
||||
* 100 scores for a platform
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @return the scores
|
||||
* @throws BadRequestException if there were no scores found
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/top/{platform}")
|
||||
public ResponseEntity<List<TrackedScoreDTO>> getTopScores(@PathVariable String platform) {
|
||||
return ResponseEntity.ok(trackedScoreService.getTopScores(Platform.Platforms.getPlatform(platform), 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* A GET mapping to retrieve the total
|
||||
* amount of scores for a platform
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @return the amount scores
|
||||
* @throws BadRequestException if there were no scores found
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/count/{platform}")
|
||||
public ResponseEntity<?> getScoresCount(@PathVariable String platform) {
|
||||
return ResponseEntity.ok(trackedScoreService.getTotalScores(Platform.Platforms.getPlatform(platform)));
|
||||
}
|
||||
}
|
39
src/main/java/cc/fascinated/controller/UserController.java
Normal file
39
src/main/java/cc/fascinated/controller/UserController.java
Normal file
@ -0,0 +1,39 @@
|
||||
package cc.fascinated.controller;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.user.User;
|
||||
import cc.fascinated.services.UserService;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/users")
|
||||
public class UserController {
|
||||
/**
|
||||
* The user service to use
|
||||
*/
|
||||
@NonNull private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(@NonNull UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* A GET mapping to retrieve a user by their steam id.
|
||||
*
|
||||
* @param id the id of the user
|
||||
* @return the user
|
||||
* @throws BadRequestException if the user is not found
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable String id) {
|
||||
return ResponseEntity.ok(userService.getUser(id));
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package cc.fascinated.exception;
|
||||
|
||||
import cc.fascinated.model.response.ErrorResponse;
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
@ControllerAdvice
|
||||
public final class ExceptionControllerAdvice {
|
||||
|
||||
/**
|
||||
* Handle a raised exception.
|
||||
*
|
||||
* @param ex the raised exception
|
||||
* @return the error response
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<?> handleException(@NonNull Exception ex) {
|
||||
HttpStatus status = null; // Get the HTTP status
|
||||
if (ex instanceof NoResourceFoundException) { // Not found
|
||||
status = HttpStatus.NOT_FOUND;
|
||||
} else if (ex instanceof UnsupportedOperationException) { // Not implemented
|
||||
status = HttpStatus.NOT_IMPLEMENTED;
|
||||
}
|
||||
if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation
|
||||
status = ex.getClass().getAnnotation(ResponseStatus.class).value();
|
||||
}
|
||||
String message = ex.getLocalizedMessage(); // Get the error message
|
||||
if (message == null) { // Fallback
|
||||
message = "An internal error has occurred.";
|
||||
}
|
||||
// Print the stack trace if no response status is present
|
||||
if (status == null) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
if (status == null) { // Fallback to 500
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
return new ResponseEntity<>(new ErrorResponse(status, message), status);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cc.fascinated.exception.impl;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public class BadRequestException extends RuntimeException {
|
||||
|
||||
public BadRequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cc.fascinated.exception.impl;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public class InternalServerErrorException extends RuntimeException {
|
||||
|
||||
public InternalServerErrorException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cc.fascinated.exception.impl;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
|
||||
public class RateLimitException extends RuntimeException {
|
||||
|
||||
public RateLimitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.fascinated.exception.impl;
|
||||
|
||||
import lombok.experimental.StandardException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@StandardException
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public class ResourceNotFoundException extends RuntimeException { }
|
@ -0,0 +1,40 @@
|
||||
package cc.fascinated.model.response;
|
||||
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Getter @ToString @EqualsAndHashCode
|
||||
public class ErrorResponse {
|
||||
/**
|
||||
* The status code of this error.
|
||||
*/
|
||||
@NonNull
|
||||
private final HttpStatus status;
|
||||
|
||||
/**
|
||||
* The HTTP code of this error.
|
||||
*/
|
||||
private final int code;
|
||||
|
||||
/**
|
||||
* The message of this error.
|
||||
*/
|
||||
@NonNull private final String message;
|
||||
|
||||
/**
|
||||
* The timestamp this error occurred.
|
||||
*/
|
||||
@NonNull private final Date timestamp;
|
||||
|
||||
public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) {
|
||||
this.status = status;
|
||||
code = status.value();
|
||||
this.message = message;
|
||||
timestamp = new Date();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package cc.fascinated.model.score;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class TotalScoresMetric {
|
||||
/**
|
||||
* The total number of scores
|
||||
*/
|
||||
private final int totalScores;
|
||||
|
||||
/**
|
||||
* The total number of ranked scores
|
||||
*/
|
||||
private final int totalRankedScores;
|
||||
}
|
96
src/main/java/cc/fascinated/model/score/TrackedScore.java
Normal file
96
src/main/java/cc/fascinated/model/score/TrackedScore.java
Normal file
@ -0,0 +1,96 @@
|
||||
package cc.fascinated.model.score;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
public class TrackedScore {
|
||||
/**
|
||||
* The ID of the score.
|
||||
*/
|
||||
@Id
|
||||
private String scoreId;
|
||||
|
||||
/**
|
||||
* The ID of the player who set the score.
|
||||
*/
|
||||
private String playerId;
|
||||
|
||||
/**
|
||||
* The ID of the leaderboard.
|
||||
*/
|
||||
private String leaderboardId;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Gets the Tracked Score as a DTO
|
||||
*/
|
||||
public TrackedScoreDTO getAsDTO() {
|
||||
return new TrackedScoreDTO(scoreId, playerId, leaderboardId, pp, rank, score, missedNotes, accuracy);
|
||||
}
|
||||
}
|
53
src/main/java/cc/fascinated/model/score/TrackedScoreDTO.java
Normal file
53
src/main/java/cc/fascinated/model/score/TrackedScoreDTO.java
Normal file
@ -0,0 +1,53 @@
|
||||
package cc.fascinated.model.score;
|
||||
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class TrackedScoreDTO {
|
||||
/**
|
||||
* The ID of the score.
|
||||
*/
|
||||
@Id
|
||||
private String scoreId;
|
||||
|
||||
/**
|
||||
* The ID of the player who set the score.
|
||||
*/
|
||||
private String playerId;
|
||||
|
||||
/**
|
||||
* The ID of the leaderboard.
|
||||
*/
|
||||
private String leaderboardId;
|
||||
|
||||
/**
|
||||
* 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 number of misses in the score.
|
||||
*/
|
||||
private Long missedNotes;
|
||||
|
||||
/**
|
||||
* The accuracy of the score.
|
||||
*/
|
||||
private Double accuracy;
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class ScoreSaberAccountToken {
|
||||
/**
|
||||
* The id for this ScoreSaber account.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The name for this account.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The profile picture for this account.
|
||||
*/
|
||||
private String profilePicture;
|
||||
|
||||
/**
|
||||
* The bio for this account.
|
||||
*/
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* The country for this account.
|
||||
*/
|
||||
private String country;
|
||||
|
||||
/**
|
||||
* The PP for this account.
|
||||
*/
|
||||
private double pp;
|
||||
|
||||
/**
|
||||
* The rank for this account.
|
||||
*/
|
||||
private int rank;
|
||||
|
||||
/**
|
||||
* The country rank for this account.
|
||||
*/
|
||||
private int countryRank;
|
||||
|
||||
/**
|
||||
* The role for this account.
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* The badges for this account.
|
||||
*/
|
||||
private Badge[] badges;
|
||||
|
||||
/**
|
||||
* The history of the rank for this account.
|
||||
*/
|
||||
private String histories;
|
||||
|
||||
/**
|
||||
* The permissions for this account.
|
||||
*/
|
||||
private int permissions;
|
||||
|
||||
/**
|
||||
* The banned status for this account.
|
||||
*/
|
||||
private boolean banned;
|
||||
|
||||
/**
|
||||
* The inactive status for this account.
|
||||
*/
|
||||
private boolean inactive;
|
||||
|
||||
/**
|
||||
* The score stats for this account.
|
||||
*/
|
||||
private ScoreStats scoreStats;
|
||||
|
||||
/**
|
||||
* The first time this account was seen.
|
||||
*/
|
||||
private String firstSeen;
|
||||
|
||||
/**
|
||||
* The badge for this account.
|
||||
*/
|
||||
@Getter
|
||||
public static class Badge {
|
||||
/**
|
||||
* The image for this badge.
|
||||
*/
|
||||
private String image;
|
||||
|
||||
/**
|
||||
* The description for this badge.
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The score stats for this account.
|
||||
*/
|
||||
@Getter
|
||||
public static class ScoreStats {
|
||||
/**
|
||||
* The total score for this account.
|
||||
*/
|
||||
private long totalScore;
|
||||
|
||||
/**
|
||||
* The total ranked score for this account.
|
||||
*/
|
||||
private long totalRankedScore;
|
||||
|
||||
/**
|
||||
* The average ranked accuracy for this account.
|
||||
*/
|
||||
private double averageRankedAccuracy;
|
||||
|
||||
/**
|
||||
* The total play count for this account.
|
||||
*/
|
||||
private int totalPlayCount;
|
||||
|
||||
/**
|
||||
* The ranked play count for this account.
|
||||
*/
|
||||
private int rankedPlayCount;
|
||||
|
||||
/**
|
||||
* The replays watched for this account.
|
||||
*/
|
||||
private int replaysWatched;
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberLeaderboardToken {
|
||||
/**
|
||||
* 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 maximum score of the song.
|
||||
*/
|
||||
private int maxScore;
|
||||
|
||||
/**
|
||||
* The date the leaderboard was created.
|
||||
*/
|
||||
private String createdDate;
|
||||
|
||||
/**
|
||||
* The date the song was ranked.
|
||||
*/
|
||||
private String rankedDate;
|
||||
|
||||
/**
|
||||
* The date the song was qualified.
|
||||
*/
|
||||
private String qualifiedDate;
|
||||
|
||||
/**
|
||||
* The date the song's status was changed to loved.
|
||||
*/
|
||||
private String lovedDate;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is ranked.
|
||||
*/
|
||||
private boolean ranked;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is qualified to be ranked.
|
||||
*/
|
||||
private boolean qualified;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard is in a loved state.
|
||||
*/
|
||||
private boolean loved;
|
||||
|
||||
/**
|
||||
* The maximum PP for this leaderboard.
|
||||
*/
|
||||
private int maxPP;
|
||||
|
||||
/**
|
||||
* The star rating for this leaderboard.
|
||||
*/
|
||||
private double stars;
|
||||
|
||||
/**
|
||||
* The amount of plays for this leaderboard.
|
||||
*/
|
||||
private int plays;
|
||||
|
||||
/**
|
||||
* The amount of daily plays for this leaderboard.
|
||||
*/
|
||||
private int dailyPlays;
|
||||
|
||||
/**
|
||||
* Whether this leaderboard has positive modifiers.
|
||||
*/
|
||||
private boolean positiveModifiers;
|
||||
|
||||
/**
|
||||
* The cover image for this leaderboard.
|
||||
*/
|
||||
private String coverImage;
|
||||
|
||||
/**
|
||||
* The difficulties for this leaderboard.
|
||||
*/
|
||||
private List<Difficulty> difficulties;
|
||||
|
||||
/**
|
||||
* The difficulty of the leaderboard.
|
||||
*/
|
||||
@Getter
|
||||
public static class Difficulty {
|
||||
/**
|
||||
* The leaderboard ID.
|
||||
*/
|
||||
private int leaderboardId;
|
||||
|
||||
/**
|
||||
* The difficulty of the leaderboard.
|
||||
*/
|
||||
private int difficulty;
|
||||
|
||||
/**
|
||||
* The game mode of the leaderboard.
|
||||
*/
|
||||
private String gameMode;
|
||||
|
||||
/**
|
||||
* The difficulty raw of the leaderboard.
|
||||
*/
|
||||
private String difficultyRaw;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberPageMetadataToken {
|
||||
/**
|
||||
* The total amount of scores.
|
||||
*/
|
||||
private int total;
|
||||
|
||||
/**
|
||||
* The current page.
|
||||
*/
|
||||
private int page;
|
||||
|
||||
/**
|
||||
* The amount of scores per page.
|
||||
*/
|
||||
private int itemsPerPage;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberPlayerScoreToken {
|
||||
/**
|
||||
* The score that was set.
|
||||
*/
|
||||
private ScoreSaberScoreToken score;
|
||||
|
||||
/**
|
||||
* The leaderboard that the score was set on.
|
||||
*/
|
||||
private ScoreSaberLeaderboardToken leaderboard;
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @ToString
|
||||
public class ScoreSaberScoreToken {
|
||||
/**
|
||||
* The id for this score.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The player info for this score.
|
||||
*/
|
||||
private LeaderboardPlayerInfo leaderboardPlayerInfo;
|
||||
|
||||
/**
|
||||
* The rank of this score.
|
||||
*/
|
||||
private int rank;
|
||||
|
||||
/**
|
||||
* The base score for this score.
|
||||
*/
|
||||
private int baseScore;
|
||||
|
||||
/**
|
||||
* The modified score for this score.
|
||||
*/
|
||||
private int modifiedScore;
|
||||
|
||||
/**
|
||||
* The PP for this score.
|
||||
*/
|
||||
private double pp;
|
||||
|
||||
/**
|
||||
* The weight for this score.
|
||||
*/
|
||||
private double weight;
|
||||
|
||||
/**
|
||||
* The modifiers for this score.
|
||||
*/
|
||||
private String modifiers;
|
||||
|
||||
/**
|
||||
* The multiplier for this score.
|
||||
*/
|
||||
private int multiplier;
|
||||
|
||||
/**
|
||||
* How many bad cuts this score has.
|
||||
*/
|
||||
private int badCuts;
|
||||
|
||||
/**
|
||||
* How many misses this score has.
|
||||
*/
|
||||
private int missedNotes;
|
||||
|
||||
/**
|
||||
* The maximum combo for this score.
|
||||
*/
|
||||
private int maxCombo;
|
||||
|
||||
/**
|
||||
* Whether this score was a full combo.
|
||||
*/
|
||||
private boolean fullCombo;
|
||||
|
||||
/**
|
||||
* The HMD that was used to set this score.
|
||||
*/
|
||||
private int hmd;
|
||||
|
||||
/**
|
||||
* The time set for this score.
|
||||
*/
|
||||
private String timeSet;
|
||||
|
||||
/**
|
||||
* Whether this score has a replay.
|
||||
*/
|
||||
private boolean hasReplay;
|
||||
|
||||
/**
|
||||
* The full HMD name that was used to set this score.
|
||||
*/
|
||||
private String deviceHmd;
|
||||
|
||||
/**
|
||||
* The controller that was used on the left hand.
|
||||
*/
|
||||
private String deviceControllerLeft;
|
||||
|
||||
/**
|
||||
* The controller that was used on the right hand.
|
||||
*/
|
||||
private String deviceControllerRight;
|
||||
|
||||
@Getter
|
||||
public class LeaderboardPlayerInfo {
|
||||
/**
|
||||
* The ID of the player.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The name of the player.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The profile picture of the player.
|
||||
*/
|
||||
private String profilePicture;
|
||||
|
||||
/**
|
||||
* The country of the player.
|
||||
*/
|
||||
private String country;
|
||||
|
||||
/**
|
||||
* The permissions for the player.
|
||||
*/
|
||||
private int permissions;
|
||||
|
||||
/**
|
||||
* The role for the player.
|
||||
*/
|
||||
private String role;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter @Setter
|
||||
@ToString
|
||||
public class ScoreSaberScoresPageToken {
|
||||
/**
|
||||
* The scores on this page.
|
||||
*/
|
||||
private ScoreSaberPlayerScoreToken[] playerScores;
|
||||
|
||||
/**
|
||||
* The metadata for this page.
|
||||
*/
|
||||
private ScoreSaberPageMetadataToken metadata;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cc.fascinated.model.token;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class ScoreSaberWebsocketDataToken {
|
||||
/**
|
||||
* The name of the command.
|
||||
*/
|
||||
private final String commandName;
|
||||
|
||||
/**
|
||||
* The data of the command.
|
||||
*/
|
||||
private final ObjectNode commandData;
|
||||
}
|
46
src/main/java/cc/fascinated/model/user/User.java
Normal file
46
src/main/java/cc/fascinated/model/user/User.java
Normal file
@ -0,0 +1,46 @@
|
||||
package cc.fascinated.model.user;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public class User {
|
||||
/**
|
||||
* The ID of the user.
|
||||
*/
|
||||
@Id
|
||||
private final UUID id;
|
||||
|
||||
/**
|
||||
* The username of the user.
|
||||
* <p>
|
||||
* Usually their Steam name.
|
||||
* </p>
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* The ID of the users steam profile.
|
||||
*/
|
||||
private String steamId;
|
||||
|
||||
/**
|
||||
* Whether the user has logged into the website.
|
||||
* <p>
|
||||
* This is used to determine if we should track their profiles or not.
|
||||
* If they haven't logged in, we don't want to track their profiles.
|
||||
* </p>
|
||||
*/
|
||||
public boolean hasLoggedIn;
|
||||
}
|
55
src/main/java/cc/fascinated/platform/Platform.java
Normal file
55
src/main/java/cc/fascinated/platform/Platform.java
Normal file
@ -0,0 +1,55 @@
|
||||
package cc.fascinated.platform;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public abstract class Platform {
|
||||
/**
|
||||
* The name of the platform.
|
||||
*/
|
||||
private final Platforms platform;
|
||||
|
||||
/**
|
||||
* Called every 10 minutes to update
|
||||
* the players data in QuestDB.
|
||||
*/
|
||||
public abstract void updatePlayers();
|
||||
|
||||
/**
|
||||
* Called every 10 minutes to update
|
||||
* the metrics for total scores, etc.
|
||||
*/
|
||||
public abstract void updateMetrics();
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum Platforms {
|
||||
SCORESABER("scoresaber");
|
||||
|
||||
/**
|
||||
* The internal name of the platform.
|
||||
*/
|
||||
private final String platformName;
|
||||
|
||||
/**
|
||||
* Gets a platform by its name.
|
||||
*
|
||||
* @param platform the name of the platform
|
||||
* @return the platform
|
||||
*/
|
||||
public static Platforms getPlatform(String platform) {
|
||||
for (Platforms platforms : Platforms.values()) {
|
||||
if (platforms.getPlatformName().equalsIgnoreCase(platform)) {
|
||||
return platforms;
|
||||
}
|
||||
}
|
||||
throw new BadRequestException("Platform '" + platform + "' was not found.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package cc.fascinated.platform.impl;
|
||||
|
||||
import cc.fascinated.model.score.TotalScoresMetric;
|
||||
import cc.fascinated.model.score.TrackedScore;
|
||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
||||
import cc.fascinated.model.user.User;
|
||||
import cc.fascinated.platform.Platform;
|
||||
import cc.fascinated.services.QuestDBService;
|
||||
import cc.fascinated.services.ScoreSaberService;
|
||||
import cc.fascinated.services.TrackedScoreService;
|
||||
import cc.fascinated.services.UserService;
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Component
|
||||
public class ScoreSaberPlatform extends Platform {
|
||||
/**
|
||||
* The ScoreSaber service to use
|
||||
*/
|
||||
@NonNull
|
||||
private final ScoreSaberService scoreSaberService;
|
||||
|
||||
/**
|
||||
* The user service to use
|
||||
*/
|
||||
@NonNull
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* The Influx service to use
|
||||
*/
|
||||
@NonNull
|
||||
private final QuestDBService questDBService;
|
||||
|
||||
/**
|
||||
* The tracked score service to use
|
||||
*/
|
||||
@NonNull
|
||||
private final TrackedScoreService trackedScoreService;
|
||||
|
||||
@Autowired
|
||||
public ScoreSaberPlatform(@NonNull ScoreSaberService scoreSaberService, @NonNull UserService userService, @NonNull QuestDBService questDBService,
|
||||
@NonNull TrackedScoreService trackedScoreService) {
|
||||
super(Platforms.SCORESABER);
|
||||
this.scoreSaberService = scoreSaberService;
|
||||
this.userService = userService;
|
||||
this.questDBService = questDBService;
|
||||
this.trackedScoreService = trackedScoreService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePlayers() {
|
||||
for (User user : this.userService.getUsers()) {
|
||||
if (!user.isHasLoggedIn()) { // Check if the user has linked their account
|
||||
continue;
|
||||
}
|
||||
ScoreSaberAccountToken account = scoreSaberService.getAccount(user); // Get the account from the ScoreSaber API
|
||||
|
||||
try (Sender sender = questDBService.getSender()) {
|
||||
sender.table("player")
|
||||
.symbol("platform", this.getPlatform().getPlatformName())
|
||||
.symbol("user_id", user.getId().toString())
|
||||
.doubleColumn("pp", account.getPp())
|
||||
.longColumn("rank", account.getRank())
|
||||
.longColumn("country_rank", account.getCountryRank())
|
||||
.longColumn("total_score", account.getScoreStats().getTotalScore())
|
||||
.longColumn("total_ranked_score", account.getScoreStats().getTotalRankedScore())
|
||||
.doubleColumn("average_ranked_accuracy", account.getScoreStats().getAverageRankedAccuracy())
|
||||
.longColumn("total_play_count", account.getScoreStats().getTotalPlayCount())
|
||||
.longColumn("ranked_play_count", account.getScoreStats().getRankedPlayCount())
|
||||
.atNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetrics() {
|
||||
try (Sender sender = questDBService.getSender()) {
|
||||
TotalScoresMetric totalScores = trackedScoreService.getTotalScores(this.getPlatform());
|
||||
sender.table("metrics")
|
||||
.symbol("platform", this.getPlatform().getPlatformName())
|
||||
.longColumn("total_scores", totalScores.getTotalScores())
|
||||
.longColumn("total_ranked_scores", totalScores.getTotalRankedScores())
|
||||
.atNow();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cc.fascinated.repository.couchdb;
|
||||
|
||||
import cc.fascinated.model.score.TrackedScore;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public interface TrackedScoreRepository extends CrudRepository<TrackedScore, String> {
|
||||
/**
|
||||
* Gets a list of top tracked scores
|
||||
* sorted by pp from the platform
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @param amount the amount of scores to get
|
||||
* @return the scores
|
||||
*/
|
||||
@Query(value = "SELECT * FROM score WHERE platform = :platform AND pp > 0 ORDER BY pp DESC LIMIT :amount", nativeQuery = true)
|
||||
List<TrackedScore> findTopRankedScores(@Param("platform") String platform, @Param("amount") int amount);
|
||||
|
||||
/**
|
||||
* Gets the total amount of scores
|
||||
* for a platform.
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @return the total amount of scores for the platform
|
||||
*/
|
||||
@Query(value = "SELECT COUNT(*) FROM score WHERE platform = :platform", nativeQuery = true)
|
||||
int countTotalScores(@Param("platform") String platform);
|
||||
|
||||
/**
|
||||
* Gets the total amount of ranked scores
|
||||
* for a platform.
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @return the total amount of ranked scores for the platform
|
||||
*/
|
||||
@Query(value = "SELECT COUNT(*) FROM score WHERE platform = :platform AND pp > 0", nativeQuery = true)
|
||||
int countTotalRankedScores(@Param("platform") String platform);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package cc.fascinated.repository.mongo;
|
||||
|
||||
import cc.fascinated.model.user.User;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
public interface UserRepository extends MongoRepository<User, UUID> {
|
||||
/**
|
||||
* Finds a user by their steam id.
|
||||
*
|
||||
* @param steamId the steam id of the user
|
||||
* @return the user
|
||||
*/
|
||||
Optional<User> findBySteamId(String steamId);
|
||||
}
|
50
src/main/java/cc/fascinated/services/PlatformService.java
Normal file
50
src/main/java/cc/fascinated/services/PlatformService.java
Normal file
@ -0,0 +1,50 @@
|
||||
package cc.fascinated.services;
|
||||
|
||||
import cc.fascinated.platform.Platform;
|
||||
import cc.fascinated.platform.impl.ScoreSaberPlatform;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service
|
||||
@Log4j2(topic = "PlatformService")
|
||||
public class PlatformService {
|
||||
/**
|
||||
* The loaded platforms.
|
||||
*/
|
||||
private final List<Platform> platforms = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
public PlatformService(@NonNull ApplicationContext context) {
|
||||
registerPlatform(context.getBean(ScoreSaberPlatform.class));
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */1 * * * *")
|
||||
public void updatePlatforms() {
|
||||
log.info("Updating %s platforms...".formatted(this.platforms.size()));
|
||||
for (Platform platform : this.platforms) {
|
||||
platform.updatePlayers();
|
||||
platform.updateMetrics();
|
||||
}
|
||||
log.info("Finished updating platforms.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a platform.
|
||||
*
|
||||
* @param platform the platform to register
|
||||
*/
|
||||
public void registerPlatform(Platform platform) {
|
||||
this.platforms.add(platform);
|
||||
}
|
||||
}
|
||||
|
55
src/main/java/cc/fascinated/services/QuestDBService.java
Normal file
55
src/main/java/cc/fascinated/services/QuestDBService.java
Normal file
@ -0,0 +1,55 @@
|
||||
package cc.fascinated.services;
|
||||
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service
|
||||
@Getter
|
||||
@Log4j2(topic = "QuestDB Service")
|
||||
public class QuestDBService {
|
||||
/*
|
||||
* The host of the QuestDB instance.
|
||||
*/
|
||||
private final String host;
|
||||
|
||||
/*
|
||||
* The username of the QuestDB instance.
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/*
|
||||
* The password of the QuestDB instance.
|
||||
*/
|
||||
private final String password;
|
||||
|
||||
@Autowired
|
||||
public QuestDBService(@Value("${questdb.host}") @NonNull String host,
|
||||
@Value("${questdb.username}") @NonNull String username,
|
||||
@Value("${questdb.password}") @NonNull String password
|
||||
) {
|
||||
this.host = host;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new sender instance for QuestDB.
|
||||
*
|
||||
* @return the sender
|
||||
*/
|
||||
public Sender getSender() {
|
||||
return Sender.builder(Sender.Transport.HTTP)
|
||||
.address(host) // set host
|
||||
.httpUsernamePassword(username, password) // set username and password
|
||||
.retryTimeoutMillis(3000) // 3 seconds
|
||||
.build();
|
||||
}
|
||||
}
|
41
src/main/java/cc/fascinated/services/ScoreSaberService.java
Normal file
41
src/main/java/cc/fascinated/services/ScoreSaberService.java
Normal file
@ -0,0 +1,41 @@
|
||||
package cc.fascinated.services;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
||||
import cc.fascinated.model.user.User;
|
||||
import kong.unirest.core.HttpResponse;
|
||||
import kong.unirest.core.Unirest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service
|
||||
public class ScoreSaberService {
|
||||
private static final String SCORESABER_API = "https://scoresaber.com/api/";
|
||||
private static final String GET_PLAYER_ENDPOINT = SCORESABER_API + "player/%s/full";
|
||||
|
||||
/**
|
||||
* Gets the ScoreSaber account for a user.
|
||||
*
|
||||
* @param user the user to get the account for
|
||||
* @return the ScoreSaber account
|
||||
* @throws BadRequestException if an error occurred while getting the account
|
||||
*/
|
||||
public ScoreSaberAccountToken getAccount(User user) {
|
||||
if (user.getSteamId() == null) {
|
||||
throw new BadRequestException("%s does not have a linked ScoreSaber account".formatted(user.getUsername()));
|
||||
}
|
||||
|
||||
HttpResponse<ScoreSaberAccountToken> response = Unirest.get(GET_PLAYER_ENDPOINT.formatted(user.getSteamId()))
|
||||
.asObject(ScoreSaberAccountToken.class);
|
||||
|
||||
if (response.getParsingError().isPresent()) { // Failed to parse the response
|
||||
throw new BadRequestException("Failed to parse ScoreSaber account for %s".formatted(user.getUsername()));
|
||||
}
|
||||
if (response.getStatus() != 200) { // The response was not successful
|
||||
throw new BadRequestException("Failed to get ScoreSaber account for %s".formatted(user.getUsername()));
|
||||
}
|
||||
return response.getBody();
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package cc.fascinated.services;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.score.TotalScoresMetric;
|
||||
import cc.fascinated.model.score.TrackedScore;
|
||||
import cc.fascinated.model.score.TrackedScoreDTO;
|
||||
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.List;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service
|
||||
public class TrackedScoreService {
|
||||
/**
|
||||
* The tracked score repository to use.
|
||||
*/
|
||||
@NonNull private final TrackedScoreRepository trackedScoreRepository;
|
||||
|
||||
@Autowired
|
||||
public TrackedScoreService(@NonNull TrackedScoreRepository trackedScoreRepository) {
|
||||
this.trackedScoreRepository = trackedScoreRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of top tracked scores
|
||||
* sorted by pp from the platform
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @param amount the amount of scores to get
|
||||
* @return the scores
|
||||
*/
|
||||
public List<TrackedScoreDTO> getTopScores(Platform.Platforms platform, int amount) {
|
||||
List<TrackedScore> scores = trackedScoreRepository.findTopRankedScores(platform.getPlatformName(), amount);
|
||||
if (scores.isEmpty()) {
|
||||
throw new BadRequestException("No scores found for platform " + platform.getPlatformName());
|
||||
}
|
||||
return scores.stream().map(TrackedScore::getAsDTO).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total amount of scores
|
||||
* for a platform.
|
||||
*
|
||||
* @param platform the platform to get the scores from
|
||||
* @return the total amount of scores for the platform
|
||||
*/
|
||||
public TotalScoresMetric getTotalScores(Platform.Platforms platform) {
|
||||
return new TotalScoresMetric(
|
||||
trackedScoreRepository.countTotalScores(platform.getPlatformName()),
|
||||
trackedScoreRepository.countTotalRankedScores(platform.getPlatformName())
|
||||
);
|
||||
}
|
||||
}
|
79
src/main/java/cc/fascinated/services/UserService.java
Normal file
79
src/main/java/cc/fascinated/services/UserService.java
Normal file
@ -0,0 +1,79 @@
|
||||
package cc.fascinated.services;
|
||||
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import cc.fascinated.model.user.User;
|
||||
import cc.fascinated.repository.mongo.UserRepository;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
/**
|
||||
* The user repository to use
|
||||
*/
|
||||
@NonNull private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserService(@NonNull UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user by their id
|
||||
*
|
||||
* @param steamId the id of the user's steam profile
|
||||
* @return the user
|
||||
* @throws BadRequestException if the user is not found
|
||||
*/
|
||||
public User getUser(String steamId) {
|
||||
if (!this.validateSteamId(steamId)) {
|
||||
throw new BadRequestException("Invalid steam id");
|
||||
}
|
||||
|
||||
Optional<User> userOptional = this.userRepository.findBySteamId(steamId);
|
||||
if (userOptional.isEmpty()) {
|
||||
// todo: check the steam API to see if the user exists
|
||||
User user = new User(UUID.randomUUID());
|
||||
user.setSteamId(steamId);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
return userOptional.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a user in the database
|
||||
*
|
||||
* @param user the user to create
|
||||
* @return the created user
|
||||
*/
|
||||
public User createUser(User user) {
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all users in the database
|
||||
*
|
||||
* @return all users
|
||||
*/
|
||||
public List<User> getUsers() {
|
||||
return (List<User>) this.userRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a steam id
|
||||
*
|
||||
* @param steamId the steam id to validate
|
||||
* @return if the steam id is valid
|
||||
*/
|
||||
public boolean validateSteamId(String steamId) {
|
||||
return steamId != null && steamId.length() == 17;
|
||||
}
|
||||
}
|
53
src/main/java/cc/fascinated/websocket/Websocket.java
Normal file
53
src/main/java/cc/fascinated/websocket/Websocket.java
Normal file
@ -0,0 +1,53 @@
|
||||
package cc.fascinated.websocket;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Log4j2
|
||||
@Getter
|
||||
public class Websocket extends TextWebSocketHandler {
|
||||
/**
|
||||
* The name of the WebSocket.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The URL of the WebSocket.
|
||||
*/
|
||||
private final String url;
|
||||
|
||||
public Websocket(@NonNull String name, @NonNull String url) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
connectWebSocket(); // Connect to the WebSocket.
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the ScoreSaber WebSocket.
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void connectWebSocket() {
|
||||
log.info("Connecting to the {}", this.getName());
|
||||
new StandardWebSocketClient().execute(this, this.getUrl()).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void afterConnectionEstablished(@NonNull WebSocketSession session) {
|
||||
log.info("Connected to the {}", this.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
|
||||
log.info("Disconnected from the {}", this.getName());
|
||||
connectWebSocket(); // Reconnect to the WebSocket.
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
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.platform.Platform;
|
||||
import cc.fascinated.services.QuestDBService;
|
||||
import cc.fascinated.services.UserService;
|
||||
import cc.fascinated.websocket.Websocket;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@Component
|
||||
@Log4j2(topic = "ScoreSaber Websocket")
|
||||
public class ScoreSaberWebsocket extends Websocket {
|
||||
/**
|
||||
* The Jackson deserializer to use.
|
||||
*/
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* The user service to use
|
||||
*/
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* The Influx service to use
|
||||
*/
|
||||
private final QuestDBService questDBService;
|
||||
|
||||
@Autowired
|
||||
public ScoreSaberWebsocket(@NonNull ObjectMapper objectMapper, @NonNull UserService userService, @NonNull QuestDBService questDBService) {
|
||||
super("ScoreSaber", "wss://scoresaber.com/ws");
|
||||
this.objectMapper = objectMapper;
|
||||
this.userService = userService;
|
||||
this.questDBService = questDBService;
|
||||
}
|
||||
|
||||
@Override @SneakyThrows
|
||||
protected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage message) {
|
||||
String payload = message.getPayload();
|
||||
if (payload.equals("Connected to the ScoreSaber WSS")) { // Ignore the connection message
|
||||
return;
|
||||
}
|
||||
|
||||
ScoreSaberWebsocketDataToken response = this.objectMapper.readValue(payload, ScoreSaberWebsocketDataToken.class);
|
||||
if (!response.getCommandName().equals("score")) { // Ignore non-score messages
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the message using Jackson
|
||||
ScoreSaberPlayerScoreToken scoreToken = this.objectMapper.readValue(response.getCommandData().toString(), ScoreSaberPlayerScoreToken.class);
|
||||
ScoreSaberScoreToken score = scoreToken.getScore();
|
||||
ScoreSaberLeaderboardToken leaderboard = scoreToken.getLeaderboard();
|
||||
ScoreSaberScoreToken.LeaderboardPlayerInfo player = score.getLeaderboardPlayerInfo();
|
||||
|
||||
if (!userService.validateSteamId(player.getId())) { // Validate the Steam ID
|
||||
return;
|
||||
}
|
||||
|
||||
double accuracy = ((double) score.getBaseScore() / leaderboard.getMaxScore()) * 100;
|
||||
String difficulty = ScoreSaberUtils.parseDifficulty(leaderboard.getDifficulty().getDifficulty());
|
||||
|
||||
try (Sender sender = questDBService.getSender()) {
|
||||
sender.table("score")
|
||||
.symbol("platform", Platform.Platforms.SCORESABER.getPlatformName())
|
||||
// Player information
|
||||
.symbol("player_id", player.getId())
|
||||
// Score information
|
||||
.symbol("leaderboard_id", leaderboard.getId())
|
||||
.symbol("score_id", score.getId())
|
||||
.doubleColumn("pp", score.getPp())
|
||||
.longColumn("rank", score.getRank())
|
||||
.longColumn("score", score.getBaseScore())
|
||||
.longColumn("modified_score", score.getModifiedScore())
|
||||
.doubleColumn("weight", score.getWeight())
|
||||
.stringColumn("modifiers", score.getModifiers())
|
||||
.doubleColumn("multiplier", score.getMultiplier())
|
||||
.longColumn("missed_notes", score.getMissedNotes())
|
||||
.longColumn("bad_cuts", score.getBadCuts())
|
||||
.longColumn("max_combo", score.getMaxCombo())
|
||||
.doubleColumn("accuracy", accuracy)
|
||||
.stringColumn("difficulty", difficulty)
|
||||
.atNow();
|
||||
}
|
||||
log.info("Tracked score for '%s' on '%s'".formatted(player.getName(), leaderboard.getSongName()));
|
||||
}
|
||||
}
|
48
src/main/resources/application.yml
Normal file
48
src/main/resources/application.yml
Normal file
@ -0,0 +1,48 @@
|
||||
# Server Configuration
|
||||
server:
|
||||
address: 0.0.0.0
|
||||
port: 7500
|
||||
|
||||
# Spring Configuration
|
||||
spring:
|
||||
data:
|
||||
# MongoDB Configuration
|
||||
mongodb:
|
||||
uri: "mongodb://bs-tracker:p4$$w0rd@localhost:27017"
|
||||
database: "bs-tracker"
|
||||
auto-index-creation: true # Automatically create collection indexes
|
||||
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/<YOUR_DATABASE_NAME>
|
||||
username: <YOUR_USERNAME>
|
||||
password: <YOUR_PASSWORD>
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: <create | create-drop | update | validate | none>
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
# Don't serialize null values by default with Jackson
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
|
||||
# QuestDB Configuration
|
||||
questdb:
|
||||
host: localhost:9000
|
||||
username: admin
|
||||
password: quest
|
||||
|
||||
# DO NOT TOUCH BELOW
|
||||
management:
|
||||
# Disable all actuator endpoints
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
exclude:
|
||||
- "*"
|
||||
# Disable default metrics
|
||||
influx:
|
||||
metrics:
|
||||
export:
|
||||
enabled: false
|
Reference in New Issue
Block a user