Merge branch 'master' into update-extensions

# Conflicts:
#	Plan/extensions/build.gradle
This commit is contained in:
Aurora Lahtela 2022-07-14 08:22:33 +03:00
commit f64290c019
38 changed files with 1338 additions and 176 deletions

View File

@ -13,6 +13,7 @@ buildscript {
plugins {
id "com.github.johnrengelman.shadow" version "7.1.2" apply false
id "java"
id 'java-library'
id "jacoco"
id "checkstyle"
id "org.sonarqube" version "3.4.0.2513"
@ -54,6 +55,7 @@ subprojects {
// Build plugins
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: "java"
apply plugin: "java-library"
apply plugin: "maven-publish"
// Report plugins
@ -100,6 +102,7 @@ subprojects {
junitVersion = "5.8.2"
mockitoVersion = "4.6.1"
testContainersVersion = "1.17.3"
swaggerVersion = "2.2.1"
}
repositories {
@ -116,8 +119,9 @@ subprojects {
dependencies {
// Dependency Injection used across the project
implementation "com.google.dagger:dagger:$daggerVersion"
shadow "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
testImplementation "com.google.dagger:dagger:$daggerVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
// Test Tooling Dependencies
@ -133,7 +137,11 @@ subprojects {
}
configurations {
testArtifacts.extendsFrom testRuntimeOnly
// Include shadowed dependencies in compile classpath of dependent modules
api.extendsFrom shadow
testArtifacts.extendsFrom testRuntimeOnly // Test classes available to other modules
testImplementation.extendsFrom shadow // Include shadowed dependencies in test classpath
}
// Test classes available to other modules
task testJar(type: Jar) {

View File

@ -5,21 +5,23 @@ repositories {
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
implementation "net.playeranalytics:platform-abstraction-layer-bukkit:$palVersion"
implementation "org.bstats:bstats-bukkit:$bstatsVersion"
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-bukkit:$palVersion"
shadow "org.bstats:bstats-bukkit:$bstatsVersion"
compileOnly "me.clip:placeholderapi:$placeholderapiVersion"
compileOnly "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation "com.destroystokyo.paper:paper-api:$paperVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
configurations = [project.configurations.shadow]
relocate 'org.bstats', 'net.playeranalytics.bstats.utilities.metrics'
relocate 'org.slf4j', 'plan.org.slf4j'
}

View File

@ -1,20 +1,21 @@
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
implementation "net.playeranalytics:platform-abstraction-layer-bungeecord:$palVersion"
implementation "org.bstats:bstats-bungeecord:$bstatsVersion"
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-bungeecord:$palVersion"
shadow "org.bstats:bstats-bungeecord:$bstatsVersion"
compileOnly "net.md-5:bungeecord-api:$bungeeVersion"
compileOnly "com.imaginarycode.minecraft:RedisBungee:$redisBungeeVersion"
testImplementation "net.md-5:bungeecord-api:$bungeeVersion"
testImplementation "com.imaginarycode.minecraft:RedisBungee:$redisBungeeVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
configurations = [project.configurations.shadow]
relocate 'org.bstats', 'net.playeranalytics.bstats.utilities.metrics'
relocate 'org.slf4j', 'plan.org.slf4j'
}

View File

@ -4,13 +4,17 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id "dev.vankka.dependencydownload.plugin" version "$dependencyDownloadVersion"
id "com.github.node-gradle.node" version "3.4.0"
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.1"
}
configurations {
// Runtime downloading scopes
mysqlDriver
sqliteDriver
testImplementation.extendsFrom mysqlDriver, sqliteDriver
compileOnly.extendsFrom mysqlDriver, sqliteDriver
compileOnly.extendsFrom mysqlDriver, sqliteDriver,
swaggerJson // swagger.json configuration
}
task generateResourceForMySQLDriver(type: GenerateDependencyDownloadResourceTask) {
@ -30,30 +34,35 @@ task generateResourceForSQLiteDriver(type: GenerateDependencyDownloadResourceTas
}
dependencies {
implementation "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
implementation project(":api")
compileOnly project(":extensions")
implementation project(path: ":extensions", configuration: 'shadow')
implementation "org.apache.commons:commons-text:$commonsTextVersion"
implementation "org.apache.commons:commons-compress:$commonsCompressVersion"
implementation "commons-codec:commons-codec:$commonsCodecVersion"
implementation "com.github.ben-manes.caffeine:caffeine:$caffeineVersion"
implementation "com.zaxxer:HikariCP:$hikariVersion"
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "com.maxmind.geoip2:geoip2:$geoIpVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
compileOnly "net.kyori:adventure-api:4.9.3"
shadow project(":extensions")
implementation("dev.vankka:dependencydownload-runtime:$dependencyDownloadVersion") {
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
compileOnly "net.kyori:adventure-api:4.9.3"
shadow("dev.vankka:dependencydownload-runtime:$dependencyDownloadVersion") {
// Effectively disables relocating
exclude module: "jar-relocator"
}
mysqlDriver "mysql:mysql-connector-java:$mysqlVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion"
implementation "org.eclipse.jetty:jetty-server:$jettyVersion"
implementation "org.eclipse.jetty:jetty-alpn-java-server:$jettyVersion"
implementation "org.eclipse.jetty.http2:http2-server:$jettyVersion"
shadow "org.apache.commons:commons-text:$commonsTextVersion"
shadow "org.apache.commons:commons-compress:$commonsCompressVersion"
shadow "commons-codec:commons-codec:$commonsCodecVersion"
shadow "com.github.ben-manes.caffeine:caffeine:$caffeineVersion"
shadow "com.zaxxer:HikariCP:$hikariVersion"
shadow "org.slf4j:slf4j-nop:$slf4jVersion"
shadow "org.slf4j:slf4j-api:$slf4jVersion"
shadow "com.maxmind.geoip2:geoip2:$geoIpVersion"
shadow "com.google.code.gson:gson:$gsonVersion"
shadow "org.eclipse.jetty:jetty-server:$jettyVersion"
shadow "org.eclipse.jetty:jetty-alpn-java-server:$jettyVersion"
shadow "org.eclipse.jetty.http2:http2-server:$jettyVersion"
// Swagger annotations
implementation "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
implementation "io.swagger.core.v3:swagger-core-jakarta:$swaggerVersion"
implementation "io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion"
testImplementation project(":api")
testImplementation "com.google.code.gson:gson:$gsonVersion"
@ -127,49 +136,40 @@ task determineWebAssetModifications {
}
}
resolve { // Swagger json generation task
outputFileName = 'swagger'
outputFormat = 'JSON'
prettyPrint = 'TRUE'
classpath = sourceSets.main.runtimeClasspath
buildClasspath = classpath
resourcePackages = [
'com.djrapitops.plan.delivery.webserver',
'com.djrapitops.plan.delivery.webserver.resolver.auth',
'com.djrapitops.plan.delivery.webserver.resolver.json',
]
outputDir = file('build/generated-resources/swagger/assets/plan/web/')
}
task swaggerJsonJar(type: Jar) {
dependsOn resolve
archiveClassifier.set("resolve")
from 'build/generated-resources/swagger'
}
artifacts {
swaggerJson swaggerJsonJar
}
processResources {
dependsOn copyYarnBuildResults, determineWebAssetModifications, generateResourceForMySQLDriver, generateResourceForSQLiteDriver
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn copyYarnBuildResults
dependsOn determineWebAssetModifications
dependsOn generateResourceForMySQLDriver
dependsOn generateResourceForSQLiteDriver
dependsOn updateVersion
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from 'build/sources/resources'
}
shadowJar {
dependsOn processResources
configurations = [project.configurations.shadow]
// Exclude these files
exclude "**/*.svg"
exclude "**/*.psd"
exclude "**/*.map"
exclude "**/module-info.class"
exclude "module-info.class"
exclude 'META-INF/versions/' // Causes Sponge to crash
exclude 'org/apache/http/**/*' // Unnecessary http client depended on by geolite2 implementation
exclude 'mozilla/**/*'
// Exclude unnecessary SQLite drivers
exclude '**/Linux/android-arm/libsqlitejdbc.so'
exclude '**/DragonFlyBSD/**/libsqlitejdbc.so'
relocate 'com.maxmind', 'plan.com.maxmind'
relocate 'com.fasterxml', 'plan.com.fasterxml'
relocate 'com.zaxxer', 'plan.com.zaxxer'
relocate 'com.google.gson', 'plan.com.google.gson'
relocate 'com.google.errorprone', 'plan.com.google.errorprone'
relocate 'org.bstats', 'plan.org.bstats'
relocate 'org.slf4j', 'plan.org.slf4j'
// Exclude test dependencies
exclude "org/junit/**/*"
exclude "org/opentest4j/**/*"
exclude "org/checkerframework/**/*"
exclude "org/apiguardian/**/*"
exclude "org/mockito/**/*"
exclude "org/selenium/**/*"
exclude "org/jayway/**/*"
exclude "google/protobuf/**/*"
exclude "jargs/gnu/**/*"
mergeServiceFiles()
}

View File

@ -169,7 +169,7 @@ public class JSONFactory {
}
}
public List<Map<String, Object>> serverPlayerKillsAsJSONMap(ServerUUID serverUUID) {
public List<Map<String, Object>> serverPlayerKillsAsJSONMaps(ServerUUID serverUUID) {
Database db = dbSystem.getDatabase();
List<PlayerKill> kills = db.query(KillQueries.fetchPlayerKillsOnServer(serverUUID, 100));
return new PlayerKillMutator(kills).toJSONAsMap(formatters);

View File

@ -185,7 +185,7 @@ public class ResponseFactory {
.setStatus(200)
.build();
} catch (UncheckedIOException e) {
return notFound404("JS File not found from jar: " + fileName + ", " + e.toString());
return notFound404("JS File not found from jar: " + fileName + ", " + e);
}
}
@ -204,7 +204,7 @@ public class ResponseFactory {
.setStatus(200)
.build();
} catch (UncheckedIOException e) {
return notFound404("CSS File not found from jar: " + fileName + ", " + e.toString());
return notFound404("CSS File not found from jar: " + fileName + ", " + e);
}
}
@ -216,7 +216,7 @@ public class ResponseFactory {
.setStatus(200)
.build();
} catch (UncheckedIOException e) {
return notFound404("Image File not found from jar: " + fileName + ", " + e.toString());
return notFound404("Image File not found from jar: " + fileName + ", " + e);
}
}
@ -239,7 +239,7 @@ public class ResponseFactory {
.setContent(getResource(fileName))
.build();
} catch (UncheckedIOException e) {
return notFound404("Font File not found from jar: " + fileName + ", " + e.toString());
return notFound404("Font File not found from jar: " + fileName + ", " + e);
}
}
@ -454,4 +454,15 @@ public class ResponseFactory {
return forInternalError(e, "Could not read " + file);
}
}
public Response reactPageResponse() {
try {
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent(getResource("index.html"))
.build();
} catch (UncheckedIOException e) {
return forInternalError(e, "Could not read index.html");
}
}
}

View File

@ -30,11 +30,17 @@ import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.delivery.webserver.resolver.*;
import com.djrapitops.plan.delivery.webserver.resolver.auth.*;
import com.djrapitops.plan.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.swagger.SwaggerJsonResolver;
import com.djrapitops.plan.delivery.webserver.resolver.swagger.SwaggerPageResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.connection.ForbiddenException;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import dagger.Lazy;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -52,6 +58,12 @@ import java.util.regex.Pattern;
* @author AuroraLS3
*/
@Singleton
@OpenAPIDefinition(info = @Info(
title = "Plan API endpoints",
description = "If authentication is enabled (see response of /v1/whoami) logging in is required for endpoints (/auth/login). Pass 'Cookie' header in the requests after login.",
contact = @Contact(name = "Github Discussions", url = "https://github.com/plan-player-analytics/Plan/discussions/categories/apis-and-development"),
license = @License(name = "GNU Lesser General Public License v3.0 (LGPLv3.0)", url = "https://github.com/plan-player-analytics/Plan/blob/master/LICENSE")
))
public class ResponseResolver {
private final QueryPageResolver queryPageResolver;
@ -67,6 +79,8 @@ public class ResponseResolver {
private final LogoutResolver logoutResolver;
private final RegisterResolver registerResolver;
private final ErrorsPageResolver errorsPageResolver;
private final SwaggerJsonResolver swaggerJsonResolver;
private final SwaggerPageResolver swaggerPageResolver;
private final ErrorLogger errorLogger;
private final ResolverService resolverService;
@ -94,6 +108,9 @@ public class ResponseResolver {
RegisterResolver registerResolver,
ErrorsPageResolver errorsPageResolver,
SwaggerJsonResolver swaggerJsonResolver,
SwaggerPageResolver swaggerPageResolver,
ErrorLogger errorLogger
) {
this.resolverService = resolverService;
@ -112,6 +129,8 @@ public class ResponseResolver {
this.logoutResolver = logoutResolver;
this.registerResolver = registerResolver;
this.errorsPageResolver = errorsPageResolver;
this.swaggerJsonResolver = swaggerJsonResolver;
this.swaggerPageResolver = swaggerPageResolver;
this.errorLogger = errorLogger;
}
@ -140,6 +159,8 @@ public class ResponseResolver {
resolverService.registerResolverForMatches(plugin, Pattern.compile(StaticResourceResolver.PATH_REGEX), staticResourceResolver);
resolverService.registerResolver(plugin, "/v1", rootJSONResolver.getResolver());
resolverService.registerResolver(plugin, "/docs/swagger.json", swaggerJsonResolver);
resolverService.registerResolver(plugin, "/docs", swaggerPageResolver);
}
private NoAuthResolver fileResolver(Supplier<Response> response) {

View File

@ -30,6 +30,13 @@ import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -37,6 +44,7 @@ import java.util.Collections;
import java.util.Optional;
@Singleton
@Path("/auth/login")
public class LoginResolver implements NoAuthResolver {
private final DBSystem dbSystem;
@ -51,6 +59,21 @@ public class LoginResolver implements NoAuthResolver {
this.activeCookieStore = activeCookieStore;
}
@POST
@Operation(
description = "Log in as user. Pass user=username&password=password in response body.",
requestBody = @RequestBody(
required = true,
content = @Content(
examples = {@ExampleObject("user=username&password=password")}
)
),
responses = {
@ApiResponse(responseCode = "200", description = "Login success, read Set-Cookie header for cookie"),
@ApiResponse(responseCode = "400", description = "Bad input user details"),
@ApiResponse(responseCode = "403", description = "Too many attempts, wait"),
}
)
@Override
public Optional<Response> resolve(Request request) {
try {

View File

@ -22,12 +22,20 @@ import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
@Path("/auth/logout")
public class LogoutResolver implements NoAuthResolver {
private final ActiveCookieStore activeCookieStore;
@ -39,6 +47,15 @@ public class LogoutResolver implements NoAuthResolver {
this.activeCookieStore = activeCookieStore;
}
@GET
@Operation(
description = "Logout the user by removing cookie",
responses = {
@ApiResponse(responseCode = "302 (success)", description = "Logout successful, redirects to /login"),
@ApiResponse(responseCode = "302 (failure)", description = "Cookie had already expired, redirects to /login"),
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
String cookies = request.getHeader("Cookie").orElse("");

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
@ -27,6 +28,15 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import com.djrapitops.plan.utilities.java.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -34,13 +44,35 @@ import java.util.Collections;
import java.util.Optional;
@Singleton
@Path("/auth/register")
public class RegisterResolver implements NoAuthResolver {
private final DBSystem dbSystem;
@Inject
public RegisterResolver(DBSystem dbSystem) {this.dbSystem = dbSystem;}
public RegisterResolver(DBSystem dbSystem) {
this.dbSystem = dbSystem;
}
@GET
@Operation(
description = "Start new registration and check if registration is complete. POST user=username&password=password to start new registration.",
responses = {
@ApiResponse(responseCode = "200 (new)", description = "New registration started (when given request body details)", content = @Content(mediaType = MimeType.JSON, examples = @ExampleObject("{\"success\": true, \"code\": \"474AF76D5362\"}"))),
@ApiResponse(responseCode = "200 (unfinished)", description = "Registration not yet completed (when given code as parameter)", content = @Content(mediaType = MimeType.JSON, examples = @ExampleObject("{\"success\": false}"))),
@ApiResponse(responseCode = "200 (completed)", description = "Registration completed (when given code as parameter)", content = @Content(mediaType = MimeType.JSON, examples = @ExampleObject("{\"success\": true}"))),
@ApiResponse(responseCode = "400", description = "Given username has already been registered", content = @Content(mediaType = MimeType.JSON, examples = @ExampleObject("{\"status\": 400, \"error\": \"User already exists!\"}"))),
},
parameters = {
@Parameter(in = ParameterIn.QUERY, name = "code", description = "Registration code for finishing registration, Check if registration is complete - success: true if yes.")
},
requestBody = @RequestBody(
description = "Register a new user",
content = @Content(
examples = @ExampleObject("user=username&password=password")
)
)
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -22,6 +22,15 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.file.PlanFiles;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -36,6 +45,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
@Singleton
@Path("/v1/errors")
public class ErrorsJSONResolver implements Resolver {
private final PlanFiles files;
@ -50,6 +60,14 @@ public class ErrorsJSONResolver implements Resolver {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
}
@GET
@Operation(
description = "Get list of Plan error logs",
responses = {
@ApiResponse(responseCode = "200", description = "List of error files and their contents", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ErrorFile.class))))
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse());
@ -79,7 +97,7 @@ public class ErrorsJSONResolver implements Resolver {
try (Stream<String> lines = Files.lines(file.toPath())) {
return lines.collect(Collectors.toList());
} catch (IOException e) {
return Collections.singletonList("Failed to read " + file.getAbsolutePath() + ": " + e.toString());
return Collections.singletonList("Failed to read " + file.getAbsolutePath() + ": " + e);
}
}

View File

@ -38,6 +38,14 @@ import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
import com.djrapitops.plan.utilities.java.Lists;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -46,6 +54,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Singleton
@Path("/v1/filters")
public class FiltersJSONResolver implements Resolver {
private final ServerInfo serverInfo;
@ -81,6 +90,14 @@ public class FiltersJSONResolver implements Resolver {
return user.hasPermission("page.players");
}
@GET
@Operation(
description = "Get list of available filters, view and graph points for visualizing the view",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = FilterResponseDto.class)))
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse());

View File

@ -29,6 +29,15 @@ import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -41,6 +50,7 @@ import java.util.Optional;
* @author AuroraLS3
*/
@Singleton
@Path("/v1/graph")
public class GraphsJSONResolver implements Resolver {
private final Identifiers identifiers;
@ -70,6 +80,40 @@ public class GraphsJSONResolver implements Resolver {
* @throws BadRequestException If 'type' parameter is not defined or supported.
* @throws BadRequestException If 'server' parameter is not defined or server is not found in database.
*/
@GET
@Operation(
description = "Get graph data",
parameters = {
@Parameter(in = ParameterIn.QUERY, name = "type", description = "Type of the graph, see https://github.com/plan-player-analytics/Plan/blob/master/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java", required = true, examples = {
@ExampleObject(value = "performance", description = "Deprecated, use optimizedPerformance"),
@ExampleObject("optimizedPerformance"),
@ExampleObject("playersOnline"),
@ExampleObject("uniqueAndNew"),
@ExampleObject("hourlyUniqueAndNew"),
@ExampleObject("serverCalendar"),
@ExampleObject("worldPie"),
@ExampleObject("activity"),
@ExampleObject("geolocation"),
@ExampleObject("aggregatedPing"),
@ExampleObject("punchCard"),
@ExampleObject("serverPie"),
@ExampleObject("joinAddressPie"),
}),
@Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
@Parameter(in = ParameterIn.QUERY, name = "timestamp", description = "Epoch millisecond for the request, newer value is wanted")
},
responses = {
@ApiResponse(responseCode = "200", description = "Graph data json", content = @Content()),
@ApiResponse(responseCode = "400", description = "'type' parameter not given", content = @Content(examples = {
@ExampleObject("{\"status\": 400, \"error\": \"'type' parameter was not defined.\"}")
})),
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.ResponseBuilder;
@ -29,6 +30,15 @@ import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.LocaleSystem;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.storage.file.Resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -42,6 +52,7 @@ import java.util.*;
* @author Kopo942
*/
@Singleton
@Path("/v1/locale/{langCode}")
public class LocaleJSONResolver implements NoAuthResolver {
private final LocaleSystem localeSystem;
@ -62,6 +73,20 @@ public class LocaleJSONResolver implements NoAuthResolver {
this.addresses = addresses;
}
@GET
@Operation(
responses = {
@ApiResponse(responseCode = "200 (/locale)", description = "List of available locales", content = @Content(mediaType = MimeType.JSON, examples = {
@ExampleObject("{\"defaultLanguage\": \"EN\", \"languages\": {\"EN\": \"English\", \"FI\": \"Finnish\"}, \"languageVersions\": {\"EN\": 1657189514266, \"FI\": 1657189514266}}")
})),
@ApiResponse(responseCode = "200 (/locale/{langCode})", description = "Contents of the locale.json file matching given langCode"),
@ApiResponse(responseCode = "404", description = "Language by langCode was not found")
},
parameters = {
@Parameter(in = ParameterIn.PATH, name = "langCode", description = "Language code. NOT REQUIRED. /v1/locale lists available language codes.", allowEmptyValue = true, example = "/v1/locale/EN")
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -27,12 +27,19 @@ import com.djrapitops.plan.settings.config.paths.ProxySettings;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
import com.djrapitops.plan.utilities.java.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
@Path("/v1/metadata")
public class MetadataJSONResolver implements NoAuthResolver {
private final PlanConfig config;
@ -47,6 +54,11 @@ public class MetadataJSONResolver implements NoAuthResolver {
this.serverInfo = serverInfo;
}
@GET
@Operation(
description = "Get metadata required for displaying Plan React frontend",
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse());

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@ -34,6 +35,15 @@ import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -47,6 +57,7 @@ import java.util.stream.Collectors;
* @author AuroraLS3
*/
@Singleton
@Path("/v1/network/performanceOverview")
public class NetworkPerformanceJSONResolver implements Resolver {
private final PlanConfig config;
@ -83,6 +94,19 @@ public class NetworkPerformanceJSONResolver implements Resolver {
return request.getUser().orElse(new WebUser("")).hasPermission("page.network");
}
@GET
@Operation(
description = "Get performance overview information for multiple servers",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, examples = {
@ExampleObject("{\"numbers\": {}}")
}))
},
parameters = {
@Parameter(in = ParameterIn.QUERY, name = "servers", required = true, description = "JSON list of server uuids (URI encoded)", example = "%5B%22a779e107-0474-4d9f-8f4d-f1efb068d32e%22%5D (is [\"a779e107-0474-4d9f-8f4d-f1efb068d32e\"])")
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
List<ServerUUID> serverUUIDs = request.getQuery().get("servers")

View File

@ -24,6 +24,15 @@ import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.identification.Identifiers;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -31,6 +40,7 @@ import java.util.Optional;
import java.util.UUID;
@Singleton
@Path("/v1/player")
public class PlayerJSONResolver implements Resolver {
private final Identifiers identifiers;
@ -58,6 +68,19 @@ public class PlayerJSONResolver implements Resolver {
return false;
}
@GET
@Operation(
description = "Get player data for visualizing a single player",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
@ApiResponse(responseCode = "400", description = "If 'player' parameter is not given")
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "player", description = "Identifier for the player", examples = {
@ExampleObject("dade56b7-366a-495a-a087-5bf0178536d4"),
@ExampleObject("AuroraLS3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -27,6 +27,15 @@ import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -39,6 +48,7 @@ import java.util.Optional;
* @author AuroraLS3
*/
@Singleton
@Path("/v1/kills")
public class PlayerKillsJSONResolver implements Resolver {
private final Identifiers identifiers;
@ -61,6 +71,23 @@ public class PlayerKillsJSONResolver implements Resolver {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
}
@GET
@Operation(
description = "Get player kill data for a server",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, examples = {
@ExampleObject("{\"player_kills\": []}")
})),
@ApiResponse(responseCode = "400 (no parameter)", description = "If 'server' parameter is not given"),
@ApiResponse(responseCode = "400 (no match)", description = "If 'server' parameter does not match an existing server")
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Identifier for the server", examples = {
@ExampleObject("dade56b7-366a-495a-a087-5bf0178536d4"),
@ExampleObject("Server 1"),
@ExampleObject("1"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));
@ -70,7 +97,7 @@ public class PlayerKillsJSONResolver implements Resolver {
ServerUUID serverUUID = identifiers.getServerUUID(request);
Optional<Long> timestamp = Identifiers.getTimestamp(request);
JSONStorage.StoredJSON storedJSON = jsonResolverService.resolve(timestamp, DataID.KILLS, serverUUID,
theUUID -> Collections.singletonMap("player_kills", jsonFactory.serverPlayerKillsAsJSONMap(theUUID))
theUUID -> Collections.singletonMap("player_kills", jsonFactory.serverPlayerKillsAsJSONMaps(theUUID))
);
return Response.builder()
.setMimeType(MimeType.JSON)

View File

@ -27,6 +27,15 @@ import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -38,6 +47,7 @@ import java.util.Optional;
* @author AuroraLS3
*/
@Singleton
@Path("/v1/players")
public class PlayersTableJSONResolver implements Resolver {
private final Identifiers identifiers;
@ -65,6 +75,19 @@ public class PlayersTableJSONResolver implements Resolver {
return user.hasPermission("page.players");
}
@GET
@Operation(
description = "Get player table data for /players page or a server",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -49,6 +49,15 @@ import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
import com.djrapitops.plan.storage.database.queries.objects.playertable.QueryTablePlayersQuery;
import com.djrapitops.plan.utilities.java.Maps;
import com.google.gson.Gson;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import net.playeranalytics.plugin.scheduling.TimeAmount;
import javax.inject.Inject;
@ -60,6 +69,7 @@ import java.text.ParseException;
import java.util.*;
@Singleton
@Path("/v1/query")
public class QueryJSONResolver implements Resolver {
private final QueryFilters filters;
@ -101,6 +111,21 @@ public class QueryJSONResolver implements Resolver {
return user.hasPermission("page.players");
}
@GET
@Operation(
description = "Perform a query or get cached results. Use q to do new query, timestamp to see cached query.",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
@ApiResponse(responseCode = "400 (invalid view)", description = "If 'view' date formats does not match afterDate dd/mm/yyyy, afterTime hh:mm, beforeDate dd/mm/yyyy, beforeTime hh:mm"),
@ApiResponse(responseCode = "400 (no query)", description = "If request body is empty and 'q' request parameter is not given"),
@ApiResponse(responseCode = "400 (invalid query)", description = "If request body is empty and 'q' json request parameter doesn't contain 'view' property"),
},
parameters = {
@Parameter(in = ParameterIn.QUERY, name = "timestamp", description = "Epoch millisecond for cached query"),
@Parameter(in = ParameterIn.QUERY, name = "q", description = "URI encoded json, alternative is to POST in request body", schema = @Schema(implementation = InputQueryDto.class))
},
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = InputQueryDto.class)))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -27,6 +27,15 @@ import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -39,6 +48,7 @@ import java.util.Optional;
* @author AuroraLS3
*/
@Singleton
@Path("/v1/sessions")
public class SessionsJSONResolver implements Resolver {
private final Identifiers identifiers;
@ -61,6 +71,20 @@ public class SessionsJSONResolver implements Resolver {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
}
@GET
@Operation(
description = "Get sessions for a server or whole network",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
@ApiResponse(responseCode = "400", description = "If 'server' parameter is not an existing server")
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -16,11 +16,19 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.version.VersionChecker;
import com.djrapitops.plan.version.VersionInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Named;
@ -33,6 +41,7 @@ import java.util.Optional;
*
* @author Kopo942
*/
@Path("/v1/version")
public class VersionJSONResolver implements Resolver {
private final VersionChecker versionChecker;
@ -52,6 +61,14 @@ public class VersionJSONResolver implements Resolver {
return true;
}
@GET
@Operation(
description = "Get Plan version and update information",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse());

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@ -23,12 +24,20 @@ import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.utilities.java.Maps;
import dagger.Lazy;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
@Path("/v1/whoami")
public class WhoAmIJSONResolver implements NoAuthResolver {
private final Lazy<WebServer> webServer;
@ -38,6 +47,18 @@ public class WhoAmIJSONResolver implements NoAuthResolver {
this.webServer = webServer;
}
@GET
@Operation(
description = "Get information about the currently logged in user",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, examples = {
@ExampleObject(value = "{\"authRequired\": false, \"loggedIn\": false}", description = "Authentication is disabled"),
@ExampleObject(value = "{\"authRequired\": true, \"loggedIn\": false}", description = "Not logged in"),
@ExampleObject(value = "{\"authRequired\": true, \"loggedIn\": true, \"user\": {}}", description = "Logged in as user"),
})),
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));

View File

@ -0,0 +1,49 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.swagger;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class SwaggerJsonResolver implements Resolver {
private final ResponseFactory responseFactory;
@Inject
public SwaggerJsonResolver(ResponseFactory responseFactory) {
this.responseFactory = responseFactory;
}
@Override
public boolean canAccess(Request request) {
return request.getUser()
.filter(user -> user.hasPermission("page.server"))
.isPresent();
}
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(responseFactory.jsonFileResponse("swagger.json"));
}
}

View File

@ -0,0 +1,49 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.swagger;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class SwaggerPageResolver implements Resolver {
private final ResponseFactory responseFactory;
@Inject
public SwaggerPageResolver(ResponseFactory responseFactory) {
this.responseFactory = responseFactory;
}
@Override
public boolean canAccess(Request request) {
return request.getUser()
.filter(user -> user.hasPermission("page.server"))
.isPresent();
}
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(responseFactory.reactPageResponse());
}
}

View File

@ -215,6 +215,8 @@ class AccessControlTest {
"/v1/locale,200",
"/v1/locale/EN,200",
"/v1/locale/NonexistingLanguage,404",
"/docs/swagger.json,500", // swagger.json not available during tests
"/docs,200",
})
void levelZeroCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel0);
@ -285,6 +287,8 @@ class AccessControlTest {
"/v1/locale,200",
"/v1/locale/EN,200",
"/v1/locale/NonexistingLanguage,404",
"/docs/swagger.json,403",
"/docs,403",
})
void levelOneCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel1);
@ -355,6 +359,8 @@ class AccessControlTest {
"/v1/locale,200",
"/v1/locale/EN,200",
"/v1/locale/NonexistingLanguage,404",
"/docs/swagger.json,403",
"/docs,403",
})
void levelTwoCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel2);
@ -423,6 +429,8 @@ class AccessControlTest {
"/v1/locale,200",
"/v1/locale/EN,200",
"/v1/locale/NonexistingLanguage,404",
"/docs/swagger.json,403",
"/docs,403",
})
void levelHundredCanNotAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel100);

View File

@ -76,8 +76,8 @@
<!-- Metrics -->
<module name="ClassFanOutComplexity">
<!-- This value is ok with manual exceptions. -->
<property name="max" value="35"/>
<!-- This value is a bit high. -->
<property name="max" value="40"/>
</module>
<module name="CyclomaticComplexity">
<!-- This value is high. Notable: ThemeConfig: 16 -->

View File

@ -1,55 +1,57 @@
dependencies {
implementation project(path: ":api")
implementation 'net.playeranalytics:Extension-AAC:4.4.2-R1.1'
implementation 'net.playeranalytics:Extension-AdvancedAchievements:6.4.0-R1.1'
implementation 'net.playeranalytics:Extension-AdvancedBan:2.1.5-R2.0'
implementation 'net.playeranalytics:Extension-ASkyBlock:3.0.9.4-R1.5'
implementation 'net.playeranalytics:Extension-AuthMe:5.6.0-R1.2'
implementation 'net.playeranalytics:Extension-BanManager:7.3.1-R1.2'
implementation 'net.playeranalytics:Extension-BentoBox:1.15.5-R2.0'
// implementation 'net.playeranalytics:Extension-CoreProtect:2.16.0-R1.4' // unable to compile new version
implementation 'net.playeranalytics:Extension-DiscordSRV:1.25.1-R1.4'
implementation 'net.playeranalytics:Extension-DKBans:2.1.2-R1.3'
implementation 'net.playeranalytics:Extension-DKCoins:3.0.5-R1.1'
implementation 'net.playeranalytics:Extension-EssentialsX:2.15.0-R2.0'
implementation 'net.playeranalytics:Extension-Factions:2.14.0-R1.1'
implementation 'net.playeranalytics:Extension-FactionsUUID:1.6.9.5-U0.5.25-R1.1'
implementation 'net.playeranalytics:Extension-FastLogin:R1.1'
implementation 'net.playeranalytics:Extension-Floodgate:2.0-R1.2'
implementation 'net.playeranalytics:Extension-GriefDefender:2.1.0-R1.1'
implementation 'net.playeranalytics:Extension-GriefPrevention:16.11.6-R1.2'
implementation 'net.playeranalytics:Extension-GriefPrevention-Sponge:4.0.1-R1.2'
implementation 'net.playeranalytics:Extension-Heroes:R1.2'
implementation 'net.playeranalytics:Extension-Jobs:4.16.3-R1.1'
implementation 'net.playeranalytics:Extension-KingdomsX:1.12.6.3.1-R1.4'
implementation 'net.playeranalytics:Extension-Lands:5.4.12-R1.1'
implementation 'net.playeranalytics:Extension-LibertyBans:0.8.0-R1.2'
implementation 'net.playeranalytics:Extension-Litebans:0.3.4-R1.4'
implementation 'net.playeranalytics:Extension-LogBlock:1.16.1.2-R1.9'
implementation 'net.playeranalytics:Extension-LuckPerms:5.0-R1.5'
implementation 'net.playeranalytics:Extension-MarriageMaster:2.3-R1.3'
implementation 'net.playeranalytics:Extension-McMMO:2.1.149-R2.5'
implementation 'net.playeranalytics:Extension-MinigamesLib:1.14.17-R1.2'
implementation 'net.playeranalytics:Extension-MyPet:3.10-R1.2'
// implementation 'net.playeranalytics:Extension-Nucleus:2.3.0-R1.1' // TODO Update to sponge 8
implementation 'net.playeranalytics:Extension-nuVotifier:2.3.4-R1.3'
implementation 'net.playeranalytics:Extension-PlaceholderAPI:2.10.9-R1.5'
implementation 'net.playeranalytics:Extension-PlotSquared:5.13.11-R1.2'
implementation 'net.playeranalytics:Extension-ProtectionStones:2.8.2-R1.2'
implementation 'net.playeranalytics:Extension-ProtocolSupport:1.16.4-R1.3'
implementation 'net.playeranalytics:Extension-Quests:4.0.5-R1.1'
implementation 'net.playeranalytics:Extension-React:6.651-R1.1'
// implementation 'net.playeranalytics:Extension-RedProtect:7.7.3-R1.1' // TODO Update to sponge 8
implementation 'net.playeranalytics:Extension-Sponge-Economy:8.0.0-R1.3'
implementation 'net.playeranalytics:Extension-SuperbVote:0.5.4-R1.1'
implementation 'net.playeranalytics:Extension-Tebex:R2.3'
implementation 'net.playeranalytics:Extension-Towny:0.96.7.4-R1.2'
implementation 'net.playeranalytics:Extension-Vault:1.7-R1.3'
implementation 'net.playeranalytics:Extension-ViaVersion:4.0.1-R1.3'
shadow 'net.playeranalytics:Extension-AAC:4.4.2-R1.1'
shadow 'net.playeranalytics:Extension-AdvancedAchievements:6.4.0-R1.1'
shadow 'net.playeranalytics:Extension-AdvancedBan:2.1.5-R2.0'
shadow 'net.playeranalytics:Extension-ASkyBlock:3.0.9.4-R1.5'
shadow 'net.playeranalytics:Extension-AuthMe:5.6.0-R1.2'
shadow 'net.playeranalytics:Extension-BanManager:7.3.1-R1.2'
shadow 'net.playeranalytics:Extension-BentoBox:1.15.5-R2.0'
// shadow 'net.playeranalytics:Extension-CoreProtect:2.16.0-R1.4' // unable to compile new version
shadow 'net.playeranalytics:Extension-DiscordSRV:1.25.1-R1.4'
shadow 'net.playeranalytics:Extension-DKBans:2.1.2-R1.3'
shadow 'net.playeranalytics:Extension-DKCoins:3.0.5-R1.1'
shadow 'net.playeranalytics:Extension-EssentialsX:2.15.0-R2.0'
shadow 'net.playeranalytics:Extension-Factions:2.14.0-R1.1'
shadow 'net.playeranalytics:Extension-FactionsUUID:1.6.9.5-U0.5.25-R1.1'
shadow 'net.playeranalytics:Extension-FastLogin:R1.1'
shadow 'net.playeranalytics:Extension-Floodgate:2.0-R1.2'
shadow 'net.playeranalytics:Extension-GriefDefender:2.1.0-R1.1'
shadow 'net.playeranalytics:Extension-GriefPrevention:16.11.6-R1.2'
shadow 'net.playeranalytics:Extension-GriefPrevention-Sponge:4.0.1-R1.2'
shadow 'net.playeranalytics:Extension-Heroes:R1.2'
shadow 'net.playeranalytics:Extension-Jobs:4.16.3-R1.1'
shadow 'net.playeranalytics:Extension-KingdomsX:1.12.6.3.1-R1.4'
shadow 'net.playeranalytics:Extension-Lands:5.4.12-R1.1'
shadow 'net.playeranalytics:Extension-LibertyBans:0.8.0-R1.2'
shadow 'net.playeranalytics:Extension-Litebans:0.3.4-R1.4'
shadow 'net.playeranalytics:Extension-LogBlock:1.16.1.2-R1.9'
shadow 'net.playeranalytics:Extension-LuckPerms:5.0-R1.5'
shadow 'net.playeranalytics:Extension-MarriageMaster:2.3-R1.3'
shadow 'net.playeranalytics:Extension-McMMO:2.1.149-R2.5'
shadow 'net.playeranalytics:Extension-MinigamesLib:1.14.17-R1.2'
shadow 'net.playeranalytics:Extension-MyPet:3.10-R1.2'
// shadow 'net.playeranalytics:Extension-Nucleus:2.3.0-R1.1' // TODO Update to sponge 8
shadow 'net.playeranalytics:Extension-nuVotifier:2.3.4-R1.3'
shadow 'net.playeranalytics:Extension-PlaceholderAPI:2.10.9-R1.5'
shadow 'net.playeranalytics:Extension-PlotSquared:5.13.11-R1.2'
shadow 'net.playeranalytics:Extension-ProtectionStones:2.8.2-R1.2'
shadow 'net.playeranalytics:Extension-ProtocolSupport:1.16.4-R1.3'
shadow 'net.playeranalytics:Extension-Quests:4.0.5-R1.1'
shadow 'net.playeranalytics:Extension-React:6.651-R1.1'
// shadow 'net.playeranalytics:Extension-RedProtect:7.7.3-R1.1' // TODO Update to sponge 8
shadow 'net.playeranalytics:Extension-Sponge-Economy:8.0.0-R1.3'
shadow 'net.playeranalytics:Extension-SuperbVote:0.5.4-R1.1'
shadow 'net.playeranalytics:Extension-Tebex:R2.3'
shadow 'net.playeranalytics:Extension-Towny:0.96.7.4-R1.2'
shadow 'net.playeranalytics:Extension-Vault:1.7-R1.3'
shadow 'net.playeranalytics:Extension-ViaVersion:4.0.1-R1.3'
}
shadowJar {
dependencies {
exclude(project(':api'))
}
configurations = [project.configurations.shadow]
// TODO Missing test scope in every Extension pom.xml causes junit to be shadowed.
exclude 'org/junit/**/*'
exclude 'org/opentest4j/**/*'
}

View File

@ -1,15 +1,17 @@
apply plugin: 'fabric-loom'
dependencies {
shadow project(path: ":api")
shadow project(path: ":extensions")
shadow project(path: ":common")
shadow project(path: ":common", configuration: "swaggerJson")
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
implementation project(path: ":common", configuration: 'shadow')
shadow project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
modImplementation('me.lucko:fabric-permissions-api:0.1-SNAPSHOT')
minecraft "com.mojang:minecraft:1.19"
mappings "net.fabricmc:yarn:1.19+build.1:v2"
modImplementation "net.fabricmc:fabric-loader:0.14.7"
modImplementation('me.lucko:fabric-permissions-api:0.1-SNAPSHOT')
// Fabric API
Set<String> apiModules = [
@ -44,6 +46,22 @@ shadowJar {
exclude('net.fabricmc:*')
exclude('/mappings/')
// Exclude these files
exclude "**/*.svg"
exclude "**/*.psd"
exclude "**/*.map"
exclude "**/module-info.class"
exclude "module-info.class"
exclude 'META-INF/versions/' // Causes Sponge to crash
exclude 'mozilla/**/*'
// Exclude extra dependencies
exclude 'org/apache/http/**/*' // Unnecessary http client depended on by geolite2 implementation
exclude "org/junit/**/*" // see extensions/build.gradle
exclude "org/opentest4j/**/*" // see extensions/build.gradle
exclude "org/checkerframework/**/*" // Dagger compiler
relocate('org.apache', 'plan.org.apache') {
exclude 'org/apache/logging/**'
}
@ -56,11 +74,22 @@ shadowJar {
relocate 'com.github.benmanes', 'plan.com.github.benmanes'
relocate 'dev.vankka.dependencydownload', 'plan.dev.vankka.dependencydownload'
relocate 'com.maxmind', 'plan.com.maxmind'
relocate 'com.fasterxml', 'plan.com.fasterxml'
relocate 'com.zaxxer', 'plan.com.zaxxer'
relocate 'com.google.gson', 'plan.com.google.gson'
relocate 'com.google.errorprone', 'plan.com.google.errorprone'
relocate 'org.bstats', 'plan.org.bstats'
relocate 'org.eclipse.jetty', 'plan.org.eclipse.jetty'
relocate 'jakarta.servlet', 'plan.jakarta.servlet'
relocate 'javax.servlet', 'plan.javax.servlet'
}
prepareRemapJar {
dependsOn tasks.shadowJar
}
remapJar {
dependsOn tasks.shadowJar
mustRunAfter tasks.shadowJar

View File

@ -1 +1 @@
org.gradle.jvmargs=-Xmx1024m
org.gradle.jvmargs=-Xmx2048m

View File

@ -4,18 +4,20 @@ repositories {
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-nukkit:$palVersion"
implementation "net.playeranalytics:platform-abstraction-layer-nukkit:$palVersion"
compileOnly "cn.nukkit:nukkit:$nukkitVersion"
compileOnly "com.creeperface.nukkit.placeholderapi:PlaceholderAPI:$nkPlaceholderapiVersion"
testImplementation "cn.nukkit:nukkit:$nukkitVersion"
testImplementation "cn.nukkit:nukkit:$nukkitVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
configurations = [project.configurations.shadow]
relocate 'org.slf4j', 'plan.org.slf4j'
}

View File

@ -1,10 +1,13 @@
dependencies {
implementation project(path: ":common", configuration: 'shadow')
implementation project(path: ":bukkit", configuration: 'shadow')
implementation project(path: ":nukkit", configuration: 'shadow')
implementation project(path: ":sponge", configuration: 'shadow')
implementation project(path: ":bungeecord", configuration: 'shadow')
implementation project(path: ":velocity", configuration: 'shadow')
shadow project(path: ":api")
shadow project(path: ":extensions")
shadow project(path: ":common")
shadow project(path: ":common", configuration: "swaggerJson")
shadow project(path: ":bukkit")
shadow project(path: ":nukkit")
shadow project(path: ":sponge")
shadow project(path: ":bungeecord")
shadow project(path: ":velocity")
testImplementation project(path: ":common", configuration: 'testArtifacts')
testImplementation project(path: ":bukkit", configuration: 'testArtifacts')
testImplementation project(path: ":nukkit", configuration: 'testArtifacts')
@ -21,6 +24,38 @@ jar {
}
shadowJar {
dependsOn processResources
configurations = [project.configurations.shadow]
// Exclude these files
exclude "**/*.svg"
exclude "**/*.psd"
exclude "**/*.map"
exclude "**/module-info.class"
exclude "module-info.class"
exclude 'META-INF/versions/' // Causes Sponge to crash
exclude 'mozilla/**/*'
// Exclude extra dependencies
exclude 'org/apache/http/**/*' // Unnecessary http client depended on by geolite2 implementation
exclude "org/junit/**/*" // see extensions/build.gradle
exclude "org/opentest4j/**/*" // see extensions/build.gradle
exclude "org/checkerframework/**/*" // Dagger compiler
exclude "com/google/common/**/*"
exclude "com/google/thirdparty/**/*"
// Exclude swagger
exclude "org/yaml/**/*"
exclude "nonapi/**/*"
exclude "io/github/classgraph/**/*"
exclude "io/swagger/**/*"
exclude "com/sun/activation/**/*"
exclude "jakarta/activation/**/*"
exclude "jakarta/validation/**/*"
exclude "jakarta/ws/**/*"
exclude "jakarta/xml/**/*"
exclude "javassist/**/*"
relocate('org.apache', 'plan.org.apache') {
exclude 'org/apache/logging/**'
exclude 'org/apache/maven/**' // This needs to be unrelocated for Sponge
@ -34,6 +69,13 @@ shadowJar {
relocate 'com.github.benmanes', 'plan.com.github.benmanes'
relocate 'dev.vankka.dependencydownload', 'plan.dev.vankka.dependencydownload'
relocate 'com.maxmind', 'plan.com.maxmind'
relocate 'com.fasterxml', 'plan.com.fasterxml'
relocate 'com.zaxxer', 'plan.com.zaxxer'
relocate 'com.google.gson', 'plan.com.google.gson'
relocate 'com.google.errorprone', 'plan.com.google.errorprone'
relocate 'org.bstats', 'plan.org.bstats'
relocate 'org.eclipse.jetty', 'plan.org.eclipse.jetty'
relocate 'jakarta.servlet', 'plan.jakarta.servlet'
relocate 'javax.servlet', 'plan.javax.servlet'
@ -42,6 +84,8 @@ shadowJar {
archiveBaseName.set('Plan')
archiveClassifier.set('')
mergeServiceFiles()
build {
dependsOn tasks.named("shadowJar")
}

View File

@ -36,6 +36,7 @@
"react-scripts": "5.0.1",
"sass": "^1.53.0",
"source-map-explorer": "^2.5.2",
"swagger-ui": "^4.12.0",
"web-vitals": "^2.1.0"
},
"scripts": {

View File

@ -29,6 +29,8 @@ import PlayersPage from "./views/layout/PlayersPage";
import AllPlayers from "./views/players/AllPlayers";
import ServerGeolocations from "./views/server/ServerGeolocations";
const SwaggerView = React.lazy(() => import("./views/SwaggerView"));
const OverviewRedirect = () => {
return (<Navigate to={"overview"} replace={true}/>)
}
@ -85,6 +87,9 @@ function App() {
<Route path="performance" element={<></>}/>
<Route path="plugins-overview" element={<></>}/>
</Route>
<Route path="docs" element={<React.Suspense fallback={<></>}>
<SwaggerView/>
</React.Suspense>}/>
</Routes>
</BrowserRouter>
</div>

View File

@ -0,0 +1,21 @@
import React, {useEffect} from "react";
import SwaggerUI from "swagger-ui"
import "swagger-ui/dist/swagger-ui.css"
import {baseAddress} from "../service/backendConfiguration"
const SwaggerView = () => {
useEffect(() => {
SwaggerUI({
dom_id: "#swagger-ui",
url: baseAddress + "/docs/swagger.json"
});
}, []);
return (
<main id="swagger-ui" className="col-12"></main>
)
}
export default SwaggerView;

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,17 @@ plugins {
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-sponge:$palVersion"
shadow "org.bstats:bstats-sponge:$bstatsVersion"
testImplementation "org.spongepowered:spongeapi:$spongeVersion"
annotationProcessor "org.spongepowered:spongeapi:$spongeVersion"
shadow "org.spongepowered:mixin:0.7.11-SNAPSHOT"
testImplementation "org.spongepowered:spongeapi:$spongeVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}

View File

@ -8,21 +8,21 @@ blossom {
}
dependencies {
compileOnly project(":common")
implementation project(path: ":common", configuration: 'shadow')
compileOnly project(":api")
implementation project(":api")
implementation project(":common")
implementation "net.playeranalytics:platform-abstraction-layer-velocity:$palVersion"
implementation "org.bstats:bstats-velocity:$bstatsVersion"
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
shadow "net.playeranalytics:platform-abstraction-layer-velocity:$palVersion"
shadow "org.bstats:bstats-velocity:$bstatsVersion"
compileOnly "com.velocitypowered:velocity-api:$velocityVersion"
testImplementation "com.velocitypowered:velocity-api:$velocityVersion"
annotationProcessor "com.velocitypowered:velocity-api:$velocityVersion"
testImplementation "com.velocitypowered:velocity-api:$velocityVersion"
testImplementation project(path: ":common", configuration: 'testArtifacts')
}
shadowJar {
configurations = [project.configurations.shadow]
relocate 'org.bstats', 'net.playeranalytics.bstats.utilities.metrics'
}