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