Compare commits
138 Commits
0228f7205d
...
renovate/i
Author | SHA1 | Date | |
---|---|---|---|
4ccd2e207a | |||
0bc614ce39 | |||
499c54c8cf | |||
41f7ca07b0 | |||
5ec61940ac | |||
6665e8a655 | |||
07562eb94d | |||
a78adf67c7 | |||
fc1f51da75 | |||
c796875d8c | |||
82fb2a3d23 | |||
2e326bb7be | |||
c5bf941c54 | |||
0eb965a26d | |||
5481c9302c | |||
b7834ab389 | |||
bb651bd88b | |||
2b017f9ef7 | |||
d83391de33 | |||
76bef70473 | |||
4aa5b0a90d | |||
6a44618ae9 | |||
146d053af8 | |||
796146c039 | |||
00c83d9ae3 | |||
3bbab24e45 | |||
5034a11e63 | |||
3d11c65678 | |||
fd3da02159 | |||
0bdaefe4a2 | |||
ba167b4e56 | |||
493e7ce4c0 | |||
7f501431b1 | |||
fa92791b56 | |||
6750773640 | |||
cecc6bc94f | |||
20db1c1aff | |||
e62e7f0fc2 | |||
c9ed681204 | |||
c6642e85fe | |||
9dfbd1af47 | |||
c8629e4f27 | |||
a6209c45ff | |||
ee5b1f12d8 | |||
ff79372ead | |||
f0e1490463 | |||
9196ec3578 | |||
a8558578f2 | |||
67efda71d2 | |||
2ad5556041 | |||
3faf2d3319 | |||
bf992713dc | |||
23e240fce1 | |||
cca45057f0 | |||
beda7fa230 | |||
d394c21f69 | |||
7df4fda744 | |||
f85ed49545 | |||
69833bf560 | |||
6693fc6793 | |||
a6ea3ab143 | |||
f96e5d5426 | |||
e360ad4446 | |||
c913816447 | |||
5dccce9fc5 | |||
3cd5e32118 | |||
5ca707bef1 | |||
6fc02cd906 | |||
f4a9d7c31c | |||
7127794152 | |||
f664406299 | |||
c5b5b3b105 | |||
b5fa470801 | |||
0854c9e76a | |||
17803410bd | |||
92fe2b28e3 | |||
eae027af84 | |||
d2ae4b4cc5 | |||
ff58b1756a | |||
543aff2a04 | |||
b666e5a8b7 | |||
ab3ed0511f | |||
bf44c9bc0d | |||
cf8e27f039 | |||
e5935c6696 | |||
5871c64582 | |||
1cfcce4806 | |||
46d4a53b11 | |||
d0cfd03ad9 | |||
4dc263961d | |||
8a8c6b542a | |||
5b8017e403 | |||
ad83e270b6 | |||
aa69970ec7 | |||
7ecaf8c580 | |||
8a985b52b8 | |||
03c679d25c | |||
6096764905 | |||
984dd8bfdc | |||
806620afff | |||
0b6c752441 | |||
f2d6200bd4 | |||
bee6f2d52d | |||
4cd345cb8f | |||
2e90f8b041 | |||
5f764d16dc | |||
4fa94c7264 | |||
7a5b42e9d7 | |||
547fa075f3 | |||
8dcde443ee | |||
ba699f5305 | |||
b3e560d1e2 | |||
cb9181010a | |||
046df7fd1f | |||
3ac4bfe2ee | |||
f037f3f9e7 | |||
04b99715c1 | |||
4e5258d74c | |||
6e336bb879 | |||
daf3770b73 | |||
02eb9e2a2e | |||
aa87d2f374 | |||
1eb540380e | |||
b0bdf6e800 | |||
597c3850e3 | |||
ce3067ee0e | |||
1c685ca414 | |||
c91a4afdf9 | |||
4fd66dffd3 | |||
b93c7f68fb | |||
d9ebbfe99e | |||
c58ceac5e4 | |||
e2d97ae417 | |||
d764242aed | |||
ebe5e763fa | |||
7e436c917a | |||
3069e9c9e8 | |||
036d8439ba |
@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
# Setup Java and Maven
|
||||
- name: Set up JDK and Maven
|
||||
uses: s4u/setup-maven-action@v1.12.0
|
||||
uses: s4u/setup-maven-action@v1.14.0
|
||||
with:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
distribution: "zulu"
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -29,3 +29,6 @@ git.properties
|
||||
pom.xml.versionsBackup
|
||||
application.yml
|
||||
target/
|
||||
|
||||
### MaxMind GeoIP2
|
||||
data/
|
||||
|
@ -1,4 +1,8 @@
|
||||
FROM maven:3.8.5-openjdk-17-slim
|
||||
FROM maven:3.9.8-eclipse-temurin-17-alpine
|
||||
|
||||
RUN apk --update --upgrade --no-cache add fontconfig ttf-freefont font-noto terminus-font \
|
||||
&& fc-cache -f \
|
||||
&& fc-list | sort
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /home/container
|
||||
@ -17,4 +21,4 @@ ENV PORT=80
|
||||
ENV ENVIRONMENT=production
|
||||
|
||||
# Run the jar file
|
||||
CMD ["java", "-jar", "target/Minecraft-Utilities.jar"]
|
||||
CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true
|
@ -1,7 +1,3 @@
|
||||
# Minecraft Utilities API
|
||||
# Minecraft Utilities - Backend
|
||||
|
||||
Wrapper for the Minecraft APIs to make them easier to use.
|
||||
|
||||
## Usage
|
||||
|
||||
View the [documentation](https://api.mcutils.xyz/swagger-ui/index.html) or visit the [website](https://api.mcutils.xyz) for more information.
|
||||
See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.
|
45
pom.xml
45
pom.xml
@ -17,7 +17,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<version>3.3.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
@ -83,17 +83,23 @@
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MongoDB for data storage -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Libraries -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.32</version>
|
||||
<version>1.18.34</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<version>2.11.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -123,17 +129,24 @@
|
||||
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentry -->
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
|
||||
<version>7.16.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- InfluxDB Metrics -->
|
||||
<dependency>
|
||||
<groupId>com.influxdb</groupId>
|
||||
<artifactId>influxdb-spring</artifactId>
|
||||
<version>7.0.0</version>
|
||||
<version>7.2.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.influxdb</groupId>
|
||||
<artifactId>influxdb-client-java</artifactId>
|
||||
<version>7.0.0</version>
|
||||
<version>7.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DNS Lookup -->
|
||||
@ -148,10 +161,24 @@
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.0.2</version>
|
||||
<version>2.6.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- GeoIP - IP Lookups -->
|
||||
<dependency>
|
||||
<groupId>com.maxmind.geoip2</groupId>
|
||||
<artifactId>geoip2</artifactId>
|
||||
<version>4.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Archive Utilities -->
|
||||
<dependency>
|
||||
<groupId>org.codehaus.plexus</groupId>
|
||||
<artifactId>plexus-archiver</artifactId>
|
||||
<version>4.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -164,6 +191,12 @@
|
||||
<version>1.4.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.flapdoodle.embed</groupId>
|
||||
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
|
||||
<version>4.16.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
6
renovate.json
Normal file
6
renovate.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
@ -12,17 +12,14 @@ import java.net.http.HttpClient;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Log4j2
|
||||
@Log4j2(topic = "Main")
|
||||
@SpringBootApplication
|
||||
public class Main {
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.setDateFormat("MM-dd-yyyy HH:mm:ss")
|
||||
.create();
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.setDateFormat("MM-dd-yyyy HH:mm:ss")
|
||||
.create();
|
||||
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
|
||||
public static final ExecutorService EXECUTOR_POOL = Executors.newFixedThreadPool(8);
|
||||
|
||||
@SneakyThrows
|
||||
public static void main(String[] args) {
|
||||
|
30
src/main/java/xyz/mcutils/backend/common/AppConfig.java
Normal file
30
src/main/java/xyz/mcutils/backend/common/AppConfig.java
Normal file
@ -0,0 +1,30 @@
|
||||
package xyz.mcutils.backend.common;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public final class AppConfig {
|
||||
/**
|
||||
* Is the app running in a production environment?
|
||||
*/
|
||||
@Getter
|
||||
private static final boolean production;
|
||||
static { // Are we running on production?
|
||||
String env = System.getenv("ENVIRONMENT");
|
||||
production = env != null && (env.equals("production"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the app running in a test environment?
|
||||
*/
|
||||
@Getter
|
||||
private static boolean isRunningTest = true;
|
||||
static {
|
||||
try {
|
||||
Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
|
||||
} catch (ClassNotFoundException e) {
|
||||
isRunningTest = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package xyz.mcutils.backend.common;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
@ -93,4 +94,19 @@ public final class ColorUtils {
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Color} from a Minecraft color code.
|
||||
*
|
||||
* @param colorCode the color code to get the color from
|
||||
* @return the color
|
||||
*/
|
||||
public static Color getMinecraftColor(char colorCode) {
|
||||
String color = COLOR_MAP.getOrDefault(colorCode, null);
|
||||
if (color == null) {
|
||||
throw new IllegalArgumentException("Invalid color code: " + colorCode);
|
||||
}
|
||||
return Color.decode(color);
|
||||
}
|
||||
|
||||
}
|
28
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal file
28
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal file
@ -0,0 +1,28 @@
|
||||
package xyz.mcutils.backend.common;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import xyz.mcutils.backend.Main;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Log4j2(topic = "Fonts")
|
||||
public class Fonts {
|
||||
|
||||
public static final Font MINECRAFT;
|
||||
public static final Font MINECRAFT_BOLD;
|
||||
public static final Font MINECRAFT_ITALIC;
|
||||
|
||||
static {
|
||||
InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
|
||||
try {
|
||||
MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
|
||||
MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
|
||||
MINECRAFT_ITALIC = MINECRAFT.deriveFont(Font.ITALIC);
|
||||
} catch (FontFormatException | IOException e) {
|
||||
log.error("Failed to load Minecraft font", e);
|
||||
throw new RuntimeException("Failed to load Minecraft font", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,21 +8,23 @@ import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
@Log4j2
|
||||
@Log4j2(topic = "Image Utils")
|
||||
public class ImageUtils {
|
||||
/**
|
||||
* Scale the given image to the provided size.
|
||||
* Scale the given image to the provided scale.
|
||||
*
|
||||
* @param image the image to scale
|
||||
* @param size the size to scale the image to
|
||||
* @param scale the scale to scale the image to
|
||||
* @return the scaled image
|
||||
*/
|
||||
public static BufferedImage resize(BufferedImage image, double size) {
|
||||
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB);
|
||||
public static BufferedImage resize(BufferedImage image, double scale) {
|
||||
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = scaled.createGraphics();
|
||||
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null);
|
||||
graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
|
||||
graphics.dispose();
|
||||
return scaled;
|
||||
}
|
||||
@ -56,4 +58,21 @@ public class ImageUtils {
|
||||
throw new Exception("Failed to convert image to bytes", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a base64 string to an image.
|
||||
*
|
||||
* @param base64 the base64 string to convert
|
||||
* @return the image
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static BufferedImage base64ToImage(String base64) {
|
||||
String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
|
||||
|
||||
try {
|
||||
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Failed to convert base64 to image", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
src/main/java/xyz/mcutils/backend/common/MojangServer.java
Normal file
85
src/main/java/xyz/mcutils/backend/common/MojangServer.java
Normal file
@ -0,0 +1,85 @@
|
||||
package xyz.mcutils.backend.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author Fascinated (fascinated7)
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@ToString
|
||||
public enum MojangServer {
|
||||
SESSION("Session Server", "https://sessionserver.mojang.com"),
|
||||
API("Mojang API", "https://api.mojang.com"),
|
||||
TEXTURES("Textures Server", "https://textures.minecraft.net"),
|
||||
ASSETS("Assets Server", "https://assets.mojang.com"),
|
||||
LIBRARIES("Libraries Server", "https://libraries.minecraft.net"),
|
||||
SERVICES("Minecraft Services", "https://api.minecraftservices.com");
|
||||
|
||||
private static final long STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
/**
|
||||
* The name of this server.
|
||||
*/
|
||||
@NonNull private final String name;
|
||||
|
||||
/**
|
||||
* The endpoint of this service.
|
||||
*/
|
||||
@NonNull private final String endpoint;
|
||||
|
||||
/**
|
||||
* Ping this service and get the status of it.
|
||||
*
|
||||
* @return the service status
|
||||
*/
|
||||
@NonNull
|
||||
public Status getStatus() {
|
||||
try {
|
||||
InetAddress address = InetAddress.getByName(endpoint.substring(8));
|
||||
long before = System.currentTimeMillis();
|
||||
if (address.isReachable((int) STATUS_TIMEOUT)) {
|
||||
// The time it took to reach the host is 75% of
|
||||
// the timeout, consider it to be degraded.
|
||||
if ((System.currentTimeMillis() - before) > STATUS_TIMEOUT * 0.75D) {
|
||||
return Status.DEGRADED;
|
||||
}
|
||||
return Status.ONLINE;
|
||||
}
|
||||
} catch (UnknownHostException ex) {
|
||||
ex.printStackTrace();
|
||||
} catch (IOException ignored) {
|
||||
// We can safely ignore any errors, we're simply checking
|
||||
// if the host is reachable, if it's not, then it's offline.
|
||||
}
|
||||
return Status.OFFLINE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of a service.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* The service is online and accessible.
|
||||
*/
|
||||
ONLINE,
|
||||
|
||||
/**
|
||||
* The service is online, but is experiencing degraded performance.
|
||||
*/
|
||||
DEGRADED,
|
||||
|
||||
/**
|
||||
* The service is offline and inaccessible.
|
||||
*/
|
||||
OFFLINE
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass @Log4j2
|
||||
@UtilityClass @Log4j2(topic = "Player Utils")
|
||||
public class PlayerUtils {
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,14 @@
|
||||
package xyz.mcutils.backend.common.renderer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public abstract class Renderer<T> {
|
||||
|
||||
/**
|
||||
* Renders the object to the specified size.
|
||||
*
|
||||
* @param input The object to render.
|
||||
* @param size The size to render the object to.
|
||||
*/
|
||||
public abstract BufferedImage render(T input, int size);
|
||||
}
|
@ -11,7 +11,7 @@ import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
@Log4j2
|
||||
@Log4j2(topic = "Skin Renderer")
|
||||
public abstract class SkinRenderer<T extends ISkinPart> {
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,176 @@
|
||||
package xyz.mcutils.backend.common.renderer.impl.server;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import xyz.mcutils.backend.Main;
|
||||
import xyz.mcutils.backend.common.ColorUtils;
|
||||
import xyz.mcutils.backend.common.Fonts;
|
||||
import xyz.mcutils.backend.common.ImageUtils;
|
||||
import xyz.mcutils.backend.common.renderer.Renderer;
|
||||
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||
import xyz.mcutils.backend.service.ServerService;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
@Log4j2
|
||||
public class ServerPreviewRenderer extends Renderer<MinecraftServer> {
|
||||
public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
|
||||
|
||||
|
||||
private static BufferedImage SERVER_BACKGROUND;
|
||||
private static BufferedImage PING_ICON;
|
||||
static {
|
||||
try {
|
||||
SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
|
||||
PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to load server preview assets", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final int fontSize = Fonts.MINECRAFT.getSize();
|
||||
private final int width = 560;
|
||||
private final int height = 64 + 3 + 3;
|
||||
private final int padding = 3;
|
||||
|
||||
@Override
|
||||
public BufferedImage render(MinecraftServer server, int size) {
|
||||
BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||
BufferedImage favicon = getServerFavicon(server);
|
||||
BufferedImage background = SERVER_BACKGROUND;
|
||||
|
||||
// Create the graphics for drawing
|
||||
Graphics2D graphics = texture.createGraphics();
|
||||
|
||||
// Set up the font
|
||||
graphics.setFont(Fonts.MINECRAFT);
|
||||
|
||||
// Draw the background
|
||||
for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
|
||||
for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
|
||||
graphics.drawImage(background, backgroundX, backgroundY, null);
|
||||
}
|
||||
}
|
||||
|
||||
int y = fontSize + 1;
|
||||
int x = 64 + 8;
|
||||
int initialX = x; // Store the initial value of x
|
||||
|
||||
// Draw the favicon
|
||||
graphics.drawImage(favicon, padding, padding, null);
|
||||
|
||||
// Draw the server hostname
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.drawString(server.getHostname(), x, y);
|
||||
|
||||
// Draw the server motd
|
||||
y += fontSize + (padding * 2);
|
||||
for (String line : server.getMotd().getRaw()) {
|
||||
int index = 0;
|
||||
int colorIndex = line.indexOf("§");
|
||||
while (colorIndex != -1) {
|
||||
// Draw text before color code
|
||||
String textBeforeColor = line.substring(index, colorIndex);
|
||||
graphics.drawString(textBeforeColor, x, y);
|
||||
// Calculate width of text before color code
|
||||
int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
|
||||
// Move x position to after the drawn text
|
||||
x += textWidth;
|
||||
// Set color based on color code
|
||||
char colorCode = Character.toLowerCase(line.charAt(colorIndex + 1));
|
||||
|
||||
// Set the color and font style
|
||||
switch (colorCode) {
|
||||
case 'l': graphics.setFont(Fonts.MINECRAFT_BOLD);
|
||||
case 'o': graphics.setFont(Fonts.MINECRAFT_ITALIC);
|
||||
default: {
|
||||
try {
|
||||
graphics.setFont(Fonts.MINECRAFT);
|
||||
Color color = ColorUtils.getMinecraftColor(colorCode);
|
||||
graphics.setColor(color);
|
||||
} catch (Exception ignored) {
|
||||
// Unknown color, can ignore the error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move index to after the color code
|
||||
index = colorIndex + 2;
|
||||
// Find next color code
|
||||
colorIndex = line.indexOf("§", index);
|
||||
}
|
||||
// Draw remaining text
|
||||
String remainingText = line.substring(index);
|
||||
graphics.drawString(remainingText, x, y);
|
||||
// Move to the next line
|
||||
y += fontSize + padding;
|
||||
// Reset x position for the next line
|
||||
x = initialX; // Reset x to its initial value
|
||||
}
|
||||
|
||||
// Ensure the font is reset
|
||||
graphics.setFont(Fonts.MINECRAFT);
|
||||
|
||||
// Render the ping
|
||||
BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
|
||||
x = width - pingIcon.getWidth() - padding;
|
||||
graphics.drawImage(pingIcon, x, padding, null);
|
||||
|
||||
// Reset the y position
|
||||
y = fontSize + 1;
|
||||
|
||||
// Render the player count
|
||||
MinecraftServer.Players players = server.getPlayers();
|
||||
String playersOnline = players.getOnline() + "";
|
||||
String playersMax = players.getMax() + "";
|
||||
|
||||
// Calculate the width of each player count element
|
||||
int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
|
||||
int slashWidth = graphics.getFontMetrics().stringWidth("/");
|
||||
int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
|
||||
|
||||
// Calculate the total width of the player count string
|
||||
int totalWidth = maxWidth + slashWidth + onlineWidth;
|
||||
|
||||
// Calculate the starting x position
|
||||
int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
|
||||
|
||||
// Render the player count elements
|
||||
graphics.setColor(Color.LIGHT_GRAY);
|
||||
graphics.drawString(playersOnline, startX, y);
|
||||
startX += onlineWidth;
|
||||
graphics.setColor(Color.DARK_GRAY);
|
||||
graphics.drawString("/", startX, y);
|
||||
startX += slashWidth;
|
||||
graphics.setColor(Color.LIGHT_GRAY);
|
||||
graphics.drawString(playersMax, startX, y);
|
||||
|
||||
return ImageUtils.resize(texture, (double) size / width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the favicon of a server.
|
||||
*
|
||||
* @param server the server to get the favicon of
|
||||
* @return the server favicon
|
||||
*/
|
||||
public BufferedImage getServerFavicon(MinecraftServer server) {
|
||||
String favicon = null;
|
||||
|
||||
// Get the server favicon
|
||||
if (server instanceof JavaMinecraftServer javaServer) {
|
||||
if (javaServer.getFavicon() != null) {
|
||||
favicon = javaServer.getFavicon().getBase64();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the default server icon
|
||||
if (favicon == null) {
|
||||
favicon = ServerService.DEFAULT_SERVER_ICON;
|
||||
}
|
||||
return ImageUtils.base64ToImage(favicon);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xyz.mcutils.backend.common.renderer.impl;
|
||||
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@ -11,7 +11,7 @@ import xyz.mcutils.backend.model.skin.Skin;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@AllArgsConstructor @Getter @Log4j2
|
||||
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Body")
|
||||
public class BodyRenderer extends SkinRenderer<ISkinPart.Custom> {
|
||||
public static final BodyRenderer INSTANCE = new BodyRenderer();
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xyz.mcutils.backend.common.renderer.impl;
|
||||
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||
|
||||
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
|
||||
import xyz.mcutils.backend.model.skin.ISkinPart;
|
@ -1,4 +1,4 @@
|
||||
package xyz.mcutils.backend.common.renderer.impl;
|
||||
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@ -10,7 +10,7 @@ import xyz.mcutils.backend.model.skin.Skin;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@AllArgsConstructor @Getter @Log4j2
|
||||
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Square")
|
||||
public class SquareRenderer extends SkinRenderer<ISkinPart.Vanilla> {
|
||||
public static final SquareRenderer INSTANCE = new SquareRenderer();
|
||||
|
@ -6,13 +6,15 @@ 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.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Getter @Log4j2
|
||||
@Getter @Log4j2(topic = "Config")
|
||||
@Configuration
|
||||
public class Config {
|
||||
public static Config INSTANCE;
|
||||
@ -23,18 +25,17 @@ public class Config {
|
||||
@Value("${public-url}")
|
||||
private String webPublicUrl;
|
||||
|
||||
/**
|
||||
* Whether the server is in production mode.
|
||||
*/
|
||||
private boolean production = false;
|
||||
|
||||
@PostConstruct
|
||||
public void onInitialize() {
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
String environmentProperty = environment.getProperty("ENVIRONMENT", "development");
|
||||
production = environmentProperty.equalsIgnoreCase("production"); // Set the production mode
|
||||
log.info("Server is running in {} mode", production ? "production" : "development");
|
||||
@Bean
|
||||
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
|
||||
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
|
||||
filterRegistrationBean.addUrlPatterns("/*");
|
||||
filterRegistrationBean.setName("etagFilter");
|
||||
return filterRegistrationBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
15
src/main/java/xyz/mcutils/backend/config/MongoConfig.java
Normal file
15
src/main/java/xyz/mcutils/backend/config/MongoConfig.java
Normal file
@ -0,0 +1,15 @@
|
||||
package xyz.mcutils.backend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
|
||||
@Configuration
|
||||
@EnableMongoRepositories(basePackages = "xyz.mcutils.backend.repository.mongo")
|
||||
public class MongoConfig {
|
||||
@Autowired
|
||||
void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
|
||||
mappingMongoConverter.setMapKeyDotReplacement("-DOT");
|
||||
}
|
||||
}
|
@ -8,12 +8,14 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Configuration
|
||||
@Log4j2(topic = "Redis")
|
||||
@EnableRedisRepositories(basePackages = "xyz.mcutils.backend.repository.redis")
|
||||
public class RedisConfig {
|
||||
/**
|
||||
* The Redis server host.
|
||||
|
@ -1,22 +0,0 @@
|
||||
package xyz.mcutils.backend.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import xyz.mcutils.backend.service.MetricService;
|
||||
import xyz.mcutils.backend.websocket.MetricsWebSocketHandler;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Autowired
|
||||
private MetricService metricService;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(new MetricsWebSocketHandler(metricService), "/websocket/metrics").setAllowedOrigins("*");
|
||||
}
|
||||
}
|
@ -2,7 +2,9 @@ package xyz.mcutils.backend.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@ -10,17 +12,21 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
|
||||
import xyz.mcutils.backend.service.MojangService;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
|
||||
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class MojangController {
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
|
||||
public class MojangController {
|
||||
@Autowired
|
||||
private MojangService mojangService;
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/status")
|
||||
public CachedEndpointStatus getStatus() {
|
||||
return mojangService.getMojangApiStatus();
|
||||
public ResponseEntity<?> getStatus() {
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
|
||||
.body(Map.of("endpoints", mojangService.getMojangServerStatus()));
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,10 @@ import xyz.mcutils.backend.service.PlayerService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
|
||||
@RequestMapping(value = "/player/")
|
||||
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
|
||||
public class PlayerController {
|
||||
|
||||
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
|
||||
private final PlayerService playerService;
|
||||
|
||||
@Autowired
|
||||
@ -32,16 +31,22 @@ public class PlayerController {
|
||||
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<?> getPlayer(
|
||||
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
|
||||
CachedPlayer player = playerService.getPlayer(id);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(cacheControl)
|
||||
.body(playerService.getPlayer(id));
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||
.body(player);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/uuid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public CachedPlayerName getPlayerUuid(
|
||||
public ResponseEntity<CachedPlayerName> getPlayerUuid(
|
||||
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
|
||||
return playerService.usernameToUuid(id);
|
||||
CachedPlayerName player = playerService.usernameToUuid(id);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePublic())
|
||||
.body(player);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/{part}/{id}")
|
||||
@ -57,7 +62,7 @@ public class PlayerController {
|
||||
|
||||
// Return the part image
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(cacheControl)
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
|
||||
.body(playerService.getSkinPart(player, part, overlays, size).getBytes());
|
||||
|
@ -3,6 +3,7 @@ package xyz.mcutils.backend.controller;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -12,10 +13,11 @@ import xyz.mcutils.backend.service.MojangService;
|
||||
import xyz.mcutils.backend.service.ServerService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
|
||||
@RequestMapping(value = "/server/")
|
||||
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
|
||||
public class ServerController {
|
||||
|
||||
private final ServerService serverService;
|
||||
@ -29,31 +31,56 @@ public class ServerController {
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/{platform}/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public CachedMinecraftServer getServer(
|
||||
public ResponseEntity<CachedMinecraftServer> getServer(
|
||||
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
|
||||
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname) {
|
||||
return serverService.getServer(platform, hostname);
|
||||
CachedMinecraftServer server = serverService.getServer(platform, hostname);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
|
||||
.body(server);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/icon/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<?> getServerIcon(
|
||||
public ResponseEntity<byte[]> getServerIcon(
|
||||
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
|
||||
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
|
||||
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
|
||||
byte[] favicon = serverService.getServerFavicon(hostname);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
|
||||
.body(serverService.getServerFavicon(hostname));
|
||||
.body(favicon);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/{platform}/preview/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<byte[]> getServerPreview(
|
||||
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
|
||||
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
|
||||
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download,
|
||||
@Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) {
|
||||
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
|
||||
CachedMinecraftServer server = serverService.getServer(platform, hostname);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
|
||||
.body(serverService.getServerPreview(server, platform, size));
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<?> getServerBlockedStatus(
|
||||
@Parameter(description = "The hostname of the server", example = "aetheria.cc") @PathVariable String hostname) {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"blocked", mojangService.isServerBlocked(hostname)
|
||||
));
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||
.body(Map.of(
|
||||
"blocked", mojangService.isServerBlocked(hostname)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package xyz.mcutils.backend.exception;
|
||||
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import io.sentry.Sentry;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
|
||||
}
|
||||
if (status == null) { // Fallback to 500
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
Sentry.captureException(ex); // Capture the exception with Sentry
|
||||
}
|
||||
return new ResponseEntity<>(new ErrorResponse(status, message), status);
|
||||
}
|
||||
|
@ -1,19 +1,22 @@
|
||||
package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import xyz.mcutils.backend.common.CachedResponse;
|
||||
import xyz.mcutils.backend.model.mojang.EndpointStatus;
|
||||
import xyz.mcutils.backend.common.MojangServer;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Setter @Getter @ToString
|
||||
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
|
||||
public class CachedEndpointStatus extends CachedResponse implements Serializable {
|
||||
|
||||
@ -26,12 +29,21 @@ public class CachedEndpointStatus extends CachedResponse implements Serializable
|
||||
/**
|
||||
* The endpoint cache.
|
||||
*/
|
||||
@JsonUnwrapped
|
||||
private final EndpointStatus value;
|
||||
private final List<Map<String, Object>> endpoints;
|
||||
|
||||
public CachedEndpointStatus(@NonNull String id, EndpointStatus value) {
|
||||
public CachedEndpointStatus(@NonNull String id, Map<MojangServer, MojangServer.Status> mojangServers) {
|
||||
super(Cache.defaultCache());
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.endpoints = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
|
||||
MojangServer server = entry.getKey();
|
||||
|
||||
Map<String, Object> serverStatus = new HashMap<>();
|
||||
serverStatus.put("name", server.getName());
|
||||
serverStatus.put("endpoint", server.getEndpoint());
|
||||
serverStatus.put("status", entry.getValue().name());
|
||||
endpoints.add(serverStatus);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,10 @@ package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import lombok.*;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import xyz.mcutils.backend.common.CachedResponse;
|
||||
@ -13,8 +16,7 @@ import java.io.Serializable;
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Setter @Getter @ToString
|
||||
@NoArgsConstructor
|
||||
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
|
||||
public class CachedMinecraftServer extends CachedResponse implements Serializable {
|
||||
/**
|
||||
|
@ -2,8 +2,8 @@ package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
@ -18,8 +18,7 @@ import java.util.UUID;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Setter @Getter
|
||||
@NoArgsConstructor
|
||||
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
|
||||
public class CachedPlayer extends CachedResponse implements Serializable {
|
||||
/**
|
||||
|
@ -1,8 +1,9 @@
|
||||
package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import xyz.mcutils.backend.common.CachedResponse;
|
||||
@ -12,8 +13,8 @@ import java.util.UUID;
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
@Setter
|
||||
@Getter @EqualsAndHashCode(callSuper = false)
|
||||
@RedisHash(value = "playerName", timeToLive = 60L * 60L * 6) // 6 hours (in seconds)
|
||||
public class CachedPlayerName extends CachedResponse {
|
||||
/**
|
||||
|
@ -1,15 +1,11 @@
|
||||
package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@Setter @Getter @EqualsAndHashCode
|
||||
@RedisHash(value = "playerSkinPart", timeToLive = 60L * 60L) // 1 hour (in seconds)
|
||||
public class CachedPlayerSkinPart {
|
||||
|
||||
|
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package xyz.mcutils.backend.model.cache;
|
||||
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Setter @Getter @EqualsAndHashCode
|
||||
@RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds)
|
||||
public class CachedServerPreview {
|
||||
|
||||
/**
|
||||
* The ID of the server preview
|
||||
*/
|
||||
@Id @NonNull private String id;
|
||||
|
||||
/**
|
||||
* The server preview bytes
|
||||
*/
|
||||
private byte[] bytes;
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
package xyz.mcutils.backend.model.dns;
|
||||
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.*;
|
||||
|
||||
@Setter @Getter
|
||||
@NoArgsConstructor @AllArgsConstructor
|
||||
@Setter @Getter @EqualsAndHashCode
|
||||
public abstract class DNSRecord {
|
||||
/**
|
||||
* The type of this record.
|
||||
|
@ -1,19 +1,32 @@
|
||||
package xyz.mcutils.backend.model.mojang;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@Getter @Setter @EqualsAndHashCode
|
||||
public class EndpointStatus {
|
||||
|
||||
/**
|
||||
* The list of endpoints and their status.
|
||||
* The name of the service.
|
||||
*/
|
||||
private final Map<String, Status> endpoints;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The hostname of the service.
|
||||
*/
|
||||
private final String hostname;
|
||||
|
||||
/**
|
||||
* The status of the service.
|
||||
*/
|
||||
private Status status;
|
||||
|
||||
/**
|
||||
* Statuses for the endpoint.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* The service is online and operational.
|
||||
|
@ -2,9 +2,11 @@ package xyz.mcutils.backend.model.player;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter @EqualsAndHashCode
|
||||
public class Cape {
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,7 @@
|
||||
package xyz.mcutils.backend.model.player;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import xyz.mcutils.backend.common.Tuple;
|
||||
@ -10,7 +11,8 @@ import xyz.mcutils.backend.model.token.MojangProfileToken;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter @AllArgsConstructor @NoArgsConstructor
|
||||
@AllArgsConstructor @NoArgsConstructor
|
||||
@Getter @EqualsAndHashCode
|
||||
public class Player {
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,14 @@
|
||||
package xyz.mcutils.backend.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
|
||||
@Getter @ToString @EqualsAndHashCode
|
||||
public class ErrorResponse {
|
||||
/**
|
||||
* The status code of this error.
|
||||
|
@ -32,8 +32,8 @@ public final class BedrockMinecraftServer extends MinecraftServer {
|
||||
|
||||
private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records,
|
||||
@NonNull Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd,
|
||||
@NonNull GameMode gamemode) {
|
||||
super(hostname, ip, port, records, motd, players);
|
||||
@NonNull GameMode gamemode, GeoLocation location) {
|
||||
super(hostname, ip, port, records, motd, players, location);
|
||||
this.id = id;
|
||||
this.edition = edition;
|
||||
this.version = version;
|
||||
@ -53,12 +53,12 @@ public final class BedrockMinecraftServer extends MinecraftServer {
|
||||
* @return the Bedrock Minecraft server
|
||||
*/
|
||||
@NonNull
|
||||
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull String token) {
|
||||
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull String token) {
|
||||
String[] split = token.split(";"); // Split the token
|
||||
Edition edition = Edition.valueOf(split[0]);
|
||||
Version version = new Version(Integer.parseInt(split[2]), split[3]);
|
||||
Players players = new Players(Integer.parseInt(split[4]), Integer.parseInt(split[5]), null);
|
||||
MOTD motd = MOTD.create(split[1] + "\n" + split[7]);
|
||||
MOTD motd = MOTD.create(hostname, Platform.BEDROCK, split[1] + "\n" + split[7]);
|
||||
GameMode gameMode = new GameMode(split[8], split.length > 9 ? Integer.parseInt(split[9]) : -1);
|
||||
return new BedrockMinecraftServer(
|
||||
split[6],
|
||||
@ -70,7 +70,8 @@ public final class BedrockMinecraftServer extends MinecraftServer {
|
||||
version,
|
||||
players,
|
||||
motd,
|
||||
gameMode
|
||||
gameMode,
|
||||
location
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import xyz.mcutils.backend.model.token.JavaServerStatusToken;
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@Setter @Getter
|
||||
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||
public final class JavaMinecraftServer extends MinecraftServer {
|
||||
|
||||
/**
|
||||
@ -65,10 +65,10 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
*/
|
||||
private boolean mojangBlocked;
|
||||
|
||||
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, DNSRecord[] records,
|
||||
@NonNull Version version, Favicon favicon, ForgeModInfo modInfo, ForgeData forgeData,
|
||||
boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
|
||||
super(hostname, ip, port, records, motd, players);
|
||||
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, GeoLocation location,
|
||||
DNSRecord[] records, @NonNull Version version, Favicon favicon, ForgeModInfo modInfo,
|
||||
ForgeData forgeData, boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
|
||||
super(hostname, ip, port, records, motd, players, location);
|
||||
this.version = version;
|
||||
this.favicon = favicon;
|
||||
this.modInfo = modInfo;
|
||||
@ -88,7 +88,7 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
* @return the Java Minecraft server
|
||||
*/
|
||||
@NonNull
|
||||
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull JavaServerStatusToken token) {
|
||||
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull JavaServerStatusToken token) {
|
||||
String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null;
|
||||
if (motdString == null) { // Not a string motd, convert from Json
|
||||
motdString = new TextComponent(ComponentSerializer.parse(Main.GSON.toJson(token.getDescription()))).toLegacyText();
|
||||
@ -97,8 +97,9 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
||||
hostname,
|
||||
ip,
|
||||
port,
|
||||
MinecraftServer.MOTD.create(motdString),
|
||||
MinecraftServer.MOTD.create(hostname, Platform.JAVA, motdString),
|
||||
token.getPlayers(),
|
||||
location,
|
||||
records,
|
||||
token.getVersion().detailedCopy(),
|
||||
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)),
|
||||
|
@ -1,11 +1,10 @@
|
||||
package xyz.mcutils.backend.model.server;
|
||||
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.*;
|
||||
import xyz.mcutils.backend.common.ColorUtils;
|
||||
import xyz.mcutils.backend.config.Config;
|
||||
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
|
||||
import xyz.mcutils.backend.service.pinger.impl.BedrockMinecraftServerPinger;
|
||||
@ -18,7 +17,7 @@ import java.util.UUID;
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter @Setter
|
||||
@Getter @Setter @EqualsAndHashCode
|
||||
public class MinecraftServer {
|
||||
|
||||
/**
|
||||
@ -51,6 +50,11 @@ public class MinecraftServer {
|
||||
*/
|
||||
private final Players players;
|
||||
|
||||
/**
|
||||
* The location of the server.
|
||||
*/
|
||||
private final GeoLocation location;
|
||||
|
||||
/**
|
||||
* A platform a Minecraft
|
||||
* server can operate on.
|
||||
@ -97,6 +101,11 @@ public class MinecraftServer {
|
||||
*/
|
||||
private final String[] html;
|
||||
|
||||
/**
|
||||
* The URL to the server preview image.
|
||||
*/
|
||||
private final String preview;
|
||||
|
||||
/**
|
||||
* Create a new MOTD from a raw string.
|
||||
*
|
||||
@ -104,12 +113,14 @@ public class MinecraftServer {
|
||||
* @return the new motd
|
||||
*/
|
||||
@NonNull
|
||||
public static MOTD create(@NonNull String raw) {
|
||||
public static MOTD create(@NonNull String hostname, @NonNull Platform platform, @NonNull String raw) {
|
||||
String[] rawLines = raw.split("\n"); // The raw lines
|
||||
return new MOTD(
|
||||
rawLines,
|
||||
Arrays.stream(rawLines).map(ColorUtils::stripColor).toArray(String[]::new),
|
||||
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new)
|
||||
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new),
|
||||
Config.INSTANCE.getWebPublicUrl() + "/server/%s/preview/%s".formatted(
|
||||
platform.name().toLowerCase(),hostname)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -150,4 +161,54 @@ public class MinecraftServer {
|
||||
@NonNull private final String name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The location of the server.
|
||||
*/
|
||||
@AllArgsConstructor @Getter
|
||||
public static class GeoLocation {
|
||||
/**
|
||||
* The country of the server.
|
||||
*/
|
||||
private final String country;
|
||||
|
||||
/**
|
||||
* The region of the server.
|
||||
*/
|
||||
private final String region;
|
||||
|
||||
/**
|
||||
* The city of the server.
|
||||
*/
|
||||
private final String city;
|
||||
|
||||
/**
|
||||
* The latitude of the server.
|
||||
*/
|
||||
private final double latitude;
|
||||
|
||||
/**
|
||||
* The longitude of the server.
|
||||
*/
|
||||
private final double longitude;
|
||||
|
||||
/**
|
||||
* Gets the location of the server from Maxmind.
|
||||
*
|
||||
* @param response the response from Maxmind
|
||||
* @return the location of the server
|
||||
*/
|
||||
public static GeoLocation fromMaxMind(CityResponse response) {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
return new GeoLocation(
|
||||
response.getCountry().getName(),
|
||||
response.getMostSpecificSubdivision().getName(),
|
||||
response.getCity().getName(),
|
||||
response.getLocation().getLatitude(),
|
||||
response.getLocation().getLongitude()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,9 @@ package xyz.mcutils.backend.model.skin;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.BodyRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.IsometricHeadRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.SquareRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer;
|
||||
import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
@ -18,7 +19,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor @NoArgsConstructor
|
||||
@Getter @Log4j2
|
||||
@Getter @Log4j2(topic = "Skin") @EqualsAndHashCode
|
||||
public class Skin {
|
||||
/**
|
||||
* The URL for the skin
|
||||
|
@ -1,13 +0,0 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
|
||||
|
||||
/**
|
||||
* A cache repository for {@link CachedEndpointStatus}'s.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Repository
|
||||
public interface EndpointStatusRepository extends CrudRepository<CachedEndpointStatus, String> { }
|
@ -1,11 +0,0 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import xyz.mcutils.backend.service.metric.Metric;
|
||||
|
||||
/**
|
||||
* A repository for {@link Metric}s.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface MetricsRepository extends CrudRepository<Metric<?>, String> { }
|
@ -0,0 +1,11 @@
|
||||
package xyz.mcutils.backend.repository.mongo;
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import xyz.mcutils.backend.service.metric.Metric;
|
||||
|
||||
/**
|
||||
* A repository for {@link Metric}s.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
public interface MetricsRepository extends MongoRepository<Metric<?>, String> { }
|
@ -1,7 +1,6 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
package xyz.mcutils.backend.repository.redis;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
||||
|
||||
/**
|
||||
@ -9,5 +8,4 @@ import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Repository
|
||||
public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { }
|
@ -1,7 +1,6 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
package xyz.mcutils.backend.repository.redis;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import xyz.mcutils.backend.model.cache.CachedPlayer;
|
||||
|
||||
import java.util.UUID;
|
||||
@ -11,5 +10,4 @@ import java.util.UUID;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Repository
|
||||
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }
|
@ -1,7 +1,6 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
package xyz.mcutils.backend.repository.redis;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import xyz.mcutils.backend.model.cache.CachedPlayerName;
|
||||
|
||||
/**
|
||||
@ -13,5 +12,4 @@ import xyz.mcutils.backend.model.cache.CachedPlayerName;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Repository
|
||||
public interface PlayerNameCacheRepository extends CrudRepository<CachedPlayerName, String> { }
|
@ -1,7 +1,6 @@
|
||||
package xyz.mcutils.backend.repository;
|
||||
package xyz.mcutils.backend.repository.redis;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart;
|
||||
|
||||
/**
|
||||
@ -11,5 +10,4 @@ import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart;
|
||||
* player skin part by it's id.
|
||||
* </p>
|
||||
*/
|
||||
@Repository
|
||||
public interface PlayerSkinPartCacheRepository extends CrudRepository<CachedPlayerSkinPart, String> { }
|
@ -0,0 +1,9 @@
|
||||
package xyz.mcutils.backend.repository.redis;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import xyz.mcutils.backend.model.cache.CachedServerPreview;
|
||||
|
||||
/**
|
||||
* A cache repository for server previews.
|
||||
*/
|
||||
public interface ServerPreviewCacheRepository extends CrudRepository<CachedServerPreview, String> { }
|
125
src/main/java/xyz/mcutils/backend/service/MaxMindService.java
Normal file
125
src/main/java/xyz/mcutils/backend/service/MaxMindService.java
Normal file
@ -0,0 +1,125 @@
|
||||
package xyz.mcutils.backend.service;
|
||||
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import io.sentry.Sentry;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.mcutils.backend.Main;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Service
|
||||
@Log4j2(topic = "MaxMind Service")
|
||||
public class MaxMindService {
|
||||
/**
|
||||
* The MaxMind database.
|
||||
*/
|
||||
private static DatabaseReader database;
|
||||
|
||||
/**
|
||||
* The location of the MaxMind database.
|
||||
*/
|
||||
private final String databaseName = "maxmind.mmdb";
|
||||
|
||||
/**
|
||||
* The MaxMind license key.
|
||||
*/
|
||||
private final String maxMindLicense;
|
||||
|
||||
public MaxMindService(@Value("${maxmind.license}") String maxMindLicense) {
|
||||
this.maxMindLicense = maxMindLicense;
|
||||
if (maxMindLicense.isBlank()) {
|
||||
log.error("The MaxMind license key is not set, please set it in the configuration and try again, disabling the MaxMind service...");
|
||||
return;
|
||||
}
|
||||
|
||||
File databaseFile = loadDatabase();
|
||||
try {
|
||||
database = new DatabaseReader.Builder(databaseFile).build();
|
||||
log.info("Loaded the MaxMind database from '{}'", databaseFile.getAbsolutePath());
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to load the MaxMind database, please check the configuration and try again", ex);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the GeoIP information for the ip.
|
||||
*
|
||||
* @param ip The query to lookup
|
||||
* @return The GeoIP information
|
||||
*/
|
||||
public static CityResponse lookup(String ip) {
|
||||
if (database == null) { // The database isn't loaded, return null
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return database.city(InetAddress.getByName(ip));
|
||||
} catch (IOException | GeoIp2Exception e) {
|
||||
log.error("Failed to lookup the GeoIP information for '{}'", ip, e);
|
||||
Sentry.captureException(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private File loadDatabase() {
|
||||
File database = new File("data", databaseName);
|
||||
if (database.exists()) {
|
||||
return database;
|
||||
}
|
||||
|
||||
// Ensure the parent directories exist
|
||||
database.getParentFile().mkdirs();
|
||||
|
||||
String downloadUrl = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz";
|
||||
HttpResponse<Path> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create(downloadUrl.formatted(maxMindLicense)))
|
||||
.build(), HttpResponse.BodyHandlers.ofFile(Files.createTempFile("maxmind", ".tar.gz")));
|
||||
Path downloadedFile = response.body();
|
||||
|
||||
File tempDir = Files.createTempDirectory("maxmind").toFile();
|
||||
|
||||
TarGZipUnArchiver archiver = new TarGZipUnArchiver();
|
||||
archiver.setSourceFile(downloadedFile.toFile());
|
||||
archiver.setDestDirectory(tempDir);
|
||||
archiver.extract();
|
||||
|
||||
File[] files = tempDir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
log.error("Failed to extract the MaxMind database");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Search for the database file
|
||||
for (File file : files) {
|
||||
// The database is in a subdirectory
|
||||
if (!file.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the database file
|
||||
File databaseFile = new File(file, "GeoLite2-City.mmdb");
|
||||
if (!databaseFile.exists()) {
|
||||
log.error("Failed to find the MaxMind database in the extracted files");
|
||||
continue;
|
||||
}
|
||||
Files.copy(databaseFile.toPath(), database.toPath());
|
||||
}
|
||||
|
||||
log.info("Downloaded and extracted the MaxMind database to '{}'", database.getAbsolutePath());
|
||||
return database;
|
||||
}
|
||||
}
|
@ -6,8 +6,9 @@ import com.influxdb.spring.influx.InfluxDB2AutoConfiguration;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.mcutils.backend.common.AppConfig;
|
||||
import xyz.mcutils.backend.common.Timer;
|
||||
import xyz.mcutils.backend.repository.MetricsRepository;
|
||||
import xyz.mcutils.backend.repository.mongo.MetricsRepository;
|
||||
import xyz.mcutils.backend.service.metric.Metric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.*;
|
||||
import xyz.mcutils.backend.service.metric.metrics.process.CpuUsageMetric;
|
||||
@ -19,7 +20,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service @Log4j2
|
||||
@Service @Log4j2(topic = "Metric Service")
|
||||
public class MetricService {
|
||||
/**
|
||||
* The metrics that are registered.
|
||||
@ -39,22 +40,35 @@ public class MetricService {
|
||||
this.influxWriteApi = influxAutoConfiguration.influxDBClient().getWriteApiBlocking();
|
||||
this.metricsRepository = metricsRepository;
|
||||
|
||||
Map<Metric<?>, Boolean> collectorEnabled = new HashMap<>();
|
||||
|
||||
// Register the metrics
|
||||
registerMetric(new TotalRequestsMetric());
|
||||
registerMetric(new RequestsPerRouteMetric());
|
||||
registerMetric(new MemoryMetric());
|
||||
registerMetric(new CpuUsageMetric());
|
||||
registerMetric(new TotalPlayerLookupsMetric());
|
||||
registerMetric(new TotalServerLookupsMetric());
|
||||
registerMetric(new ConnectedSocketsMetric());
|
||||
registerMetric(new UniquePlayerLookupsMetric());
|
||||
registerMetric(new UniqueServerLookupsMetric());
|
||||
|
||||
// Load the metrics from Redis
|
||||
loadMetrics();
|
||||
// please god forgive my sins; this is the worst code I've ever written
|
||||
for (Metric<?> metric : metrics.values()) {
|
||||
collectorEnabled.put(metric, metric.isCollector());
|
||||
}
|
||||
|
||||
Timer.scheduleRepeating(() -> {
|
||||
saveMetrics();
|
||||
writeToInflux();
|
||||
}, saveInterval, saveInterval);
|
||||
if (!AppConfig.isRunningTest()) {
|
||||
// Load the metrics from Redis
|
||||
loadMetrics();
|
||||
|
||||
for (Map.Entry<Metric<?>, Boolean> entry : collectorEnabled.entrySet()) {
|
||||
entry.getKey().setCollector(entry.getValue());
|
||||
}
|
||||
|
||||
Timer.scheduleRepeating(() -> {
|
||||
saveMetrics();
|
||||
writeToInflux();
|
||||
}, saveInterval, saveInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,8 +102,8 @@ public class MetricService {
|
||||
*/
|
||||
public void loadMetrics() {
|
||||
log.info("Loading metrics");
|
||||
for (Metric<?> metric : metricsRepository.findAll()) {
|
||||
metrics.put(metric.getClass(), metric);
|
||||
for (Metric<?> metric : metrics.values()) {
|
||||
metricsRepository.findById(metric.getId()).ifPresent(loaded -> metrics.put(loaded.getClass(), loaded));
|
||||
}
|
||||
log.info("Loaded {} metrics", metrics.size());
|
||||
}
|
||||
@ -101,7 +115,7 @@ public class MetricService {
|
||||
for (Metric<?> metric : metrics.values()) {
|
||||
saveMetric(metric);
|
||||
}
|
||||
log.info("Saved {} metrics to Redis", metrics.size());
|
||||
log.info("Saved {} metrics to MongoDB", metrics.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,24 +124,32 @@ public class MetricService {
|
||||
* @param metric the metric to save
|
||||
*/
|
||||
private void saveMetric(Metric<?> metric) {
|
||||
metricsRepository.save(metric); // Save the metric to the repository
|
||||
try {
|
||||
metricsRepository.save(metric); // Save the metric to the repository
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save metric to MongoDB", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push all metrics to InfluxDB.
|
||||
*/
|
||||
private void writeToInflux() {
|
||||
List<Point> points = new ArrayList<>();
|
||||
for (Metric<?> metric : metrics.values()) {
|
||||
if (metric.isCollector()) {
|
||||
metric.collect();
|
||||
}
|
||||
Point point = metric.toPoint();
|
||||
if (point != null) {
|
||||
points.add(point);
|
||||
try {
|
||||
List<Point> points = new ArrayList<>();
|
||||
for (Metric<?> metric : metrics.values()) {
|
||||
if (metric.isCollector()) {
|
||||
metric.collect();
|
||||
}
|
||||
Point point = metric.toPoint();
|
||||
if (point != null) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
influxWriteApi.writePoints(points);
|
||||
log.info("Wrote {} metrics to Influx", metrics.size());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to write metrics to Influx", e);
|
||||
}
|
||||
influxWriteApi.writePoints(points);
|
||||
log.info("Wrote {} metrics to Influx", metrics.size());
|
||||
}
|
||||
}
|
||||
|
@ -9,29 +9,23 @@ import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import net.jodah.expiringmap.ExpirationPolicy;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.mcutils.backend.Main;
|
||||
import xyz.mcutils.backend.common.Endpoint;
|
||||
import xyz.mcutils.backend.common.ExpiringSet;
|
||||
import xyz.mcutils.backend.common.MojangServer;
|
||||
import xyz.mcutils.backend.common.WebRequest;
|
||||
import xyz.mcutils.backend.config.Config;
|
||||
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
|
||||
import xyz.mcutils.backend.model.mojang.EndpointStatus;
|
||||
import xyz.mcutils.backend.model.token.MojangProfileToken;
|
||||
import xyz.mcutils.backend.model.token.MojangUsernameToUuidToken;
|
||||
import xyz.mcutils.backend.repository.EndpointStatusRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service @Log4j2 @Getter
|
||||
@Service
|
||||
@Log4j2(topic = "Mojang Service")
|
||||
@Getter
|
||||
public class MojangService {
|
||||
|
||||
/**
|
||||
@ -53,21 +47,9 @@ public class MojangService {
|
||||
private static final long FETCH_BLOCKED_SERVERS_INTERVAL = TimeUnit.HOURS.toMillis(1L);
|
||||
|
||||
/**
|
||||
* Information about the Mojang API endpoints.
|
||||
* The interval to fetch the Mojang server status.
|
||||
*/
|
||||
private static final String MOJANG_ENDPOINT_STATUS_KEY = "mojang";
|
||||
private static final List<Endpoint> MOJANG_ENDPOINTS = List.of(
|
||||
new Endpoint("https://textures.minecraft.net", List.of(HttpStatus.BAD_REQUEST)),
|
||||
new Endpoint("https://session.minecraft.net", List.of(HttpStatus.NOT_FOUND)),
|
||||
new Endpoint("https://libraries.minecraft.net", List.of(HttpStatus.NOT_FOUND)),
|
||||
new Endpoint("https://assets.mojang.com", List.of(HttpStatus.NOT_FOUND)),
|
||||
new Endpoint("https://api.minecraftservices.com", List.of(HttpStatus.FORBIDDEN)),
|
||||
new Endpoint(API_ENDPOINT, List.of(HttpStatus.OK)),
|
||||
new Endpoint(SESSION_SERVER_ENDPOINT, List.of(HttpStatus.FORBIDDEN))
|
||||
);
|
||||
|
||||
@Autowired
|
||||
private EndpointStatusRepository mojangEndpointStatusRepository;
|
||||
private static final long FETCH_MOJANG_SERVERS_STATUS_INTERVAL = TimeUnit.MINUTES.toMillis(1L);
|
||||
|
||||
/**
|
||||
* A list of banned server hashes provided by Mojang.
|
||||
@ -87,6 +69,11 @@ public class MojangService {
|
||||
*/
|
||||
private final ExpiringSet<String> blockedServersCache = new ExpiringSet<>(ExpirationPolicy.CREATED, 10L, TimeUnit.MINUTES);
|
||||
|
||||
/**
|
||||
* The status of the Mojang API.
|
||||
*/
|
||||
private final List<Map<String, Object>> mojangServerStatus = new ArrayList<>();
|
||||
|
||||
public MojangService() {
|
||||
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
@ -94,6 +81,33 @@ public class MojangService {
|
||||
fetchBlockedServers();
|
||||
}
|
||||
}, 0L, FETCH_BLOCKED_SERVERS_INTERVAL);
|
||||
|
||||
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("Fetching Mojang Server status...");
|
||||
Map<MojangServer, MojangServer.Status> mojangServers = new HashMap<>();
|
||||
Arrays.stream(MojangServer.values()).parallel().forEach(server -> {
|
||||
log.info("Pinging {}...", server.getEndpoint());
|
||||
MojangServer.Status status = server.getStatus(); // Retrieve the server status
|
||||
log.info("Retrieved status of {}: {}", server.getEndpoint(), status.name());
|
||||
mojangServers.put(server, status); // Cache the server status
|
||||
});
|
||||
|
||||
mojangServerStatus.clear();
|
||||
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
|
||||
MojangServer server = entry.getKey();
|
||||
|
||||
Map<String, Object> serverStatus = new HashMap<>();
|
||||
serverStatus.put("name", server.getName());
|
||||
serverStatus.put("endpoint", server.getEndpoint());
|
||||
serverStatus.put("status", entry.getValue().name());
|
||||
mojangServerStatus.add(serverStatus);
|
||||
}
|
||||
|
||||
log.info("Fetched Mojang Server status for {} endpoints", mojangServers.size());
|
||||
}
|
||||
}, 0L, FETCH_MOJANG_SERVERS_STATUS_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +118,7 @@ public class MojangService {
|
||||
log.info("Fetching blocked servers from Mojang");
|
||||
try (
|
||||
InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream();
|
||||
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n");
|
||||
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n")
|
||||
) {
|
||||
List<String> hashes = new ArrayList<>();
|
||||
while (scanner.hasNext()) {
|
||||
@ -112,6 +126,8 @@ public class MojangService {
|
||||
}
|
||||
bannedServerHashes = Collections.synchronizedList(hashes);
|
||||
log.info("Fetched {} banned server hashes", bannedServerHashes.size());
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to fetch blocked servers from Mojang", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,62 +198,6 @@ public class MojangService {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of the Mojang APIs.
|
||||
*
|
||||
* @return the status
|
||||
*/
|
||||
public CachedEndpointStatus getMojangApiStatus() {
|
||||
log.info("Getting Mojang API status");
|
||||
Optional<CachedEndpointStatus> endpointStatus = mojangEndpointStatusRepository.findById(MOJANG_ENDPOINT_STATUS_KEY);
|
||||
if (endpointStatus.isPresent() && Config.INSTANCE.isProduction()) {
|
||||
log.info("Got cached Mojang API status");
|
||||
return endpointStatus.get();
|
||||
}
|
||||
|
||||
// Fetch the status of the Mojang API endpoints
|
||||
List<CompletableFuture<EndpointStatus.Status>> futures = new ArrayList<>();
|
||||
for (Endpoint endpoint : MOJANG_ENDPOINTS) {
|
||||
CompletableFuture<EndpointStatus.Status> future = CompletableFuture.supplyAsync(() -> {
|
||||
boolean online = false;
|
||||
long start = System.currentTimeMillis();
|
||||
ResponseEntity<?> response = WebRequest.head(endpoint.getEndpoint(), String.class);
|
||||
if (endpoint.getAllowedStatuses().contains(response.getStatusCode())) {
|
||||
online = true;
|
||||
}
|
||||
if (online && System.currentTimeMillis() - start > 1000) { // If the response took longer than 1 second
|
||||
return EndpointStatus.Status.DEGRADED;
|
||||
}
|
||||
return online ? EndpointStatus.Status.ONLINE : EndpointStatus.Status.OFFLINE;
|
||||
}, Main.EXECUTOR_POOL);
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
|
||||
try {
|
||||
allFutures.get(5, TimeUnit.SECONDS); // Wait for the futures to complete
|
||||
} catch (Exception e) {
|
||||
log.error("Timeout while fetching Mojang API status: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Process the results
|
||||
Map<String, EndpointStatus.Status> endpoints = new HashMap<>();
|
||||
for (int i = 0; i < MOJANG_ENDPOINTS.size(); i++) {
|
||||
Endpoint endpoint = MOJANG_ENDPOINTS.get(i);
|
||||
EndpointStatus.Status status = futures.get(i).join();
|
||||
endpoints.put(endpoint.getEndpoint(), status);
|
||||
}
|
||||
|
||||
log.info("Fetched Mojang API status for {} endpoints", endpoints.size());
|
||||
CachedEndpointStatus status = new CachedEndpointStatus(
|
||||
MOJANG_ENDPOINT_STATUS_KEY,
|
||||
new EndpointStatus(endpoints)
|
||||
);
|
||||
mojangEndpointStatusRepository.save(status);
|
||||
status.getCache().setCached(false);
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Session Server profile of the
|
||||
* player with the given UUID.
|
||||
|
@ -3,11 +3,7 @@ package xyz.mcutils.backend.service;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.mcutils.backend.common.ImageUtils;
|
||||
import xyz.mcutils.backend.common.PlayerUtils;
|
||||
import xyz.mcutils.backend.common.Tuple;
|
||||
import xyz.mcutils.backend.common.UUIDUtils;
|
||||
import xyz.mcutils.backend.config.Config;
|
||||
import xyz.mcutils.backend.common.*;
|
||||
import xyz.mcutils.backend.exception.impl.BadRequestException;
|
||||
import xyz.mcutils.backend.exception.impl.MojangAPIRateLimitException;
|
||||
import xyz.mcutils.backend.exception.impl.RateLimitException;
|
||||
@ -21,17 +17,16 @@ import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||
import xyz.mcutils.backend.model.skin.Skin;
|
||||
import xyz.mcutils.backend.model.token.MojangProfileToken;
|
||||
import xyz.mcutils.backend.model.token.MojangUsernameToUuidToken;
|
||||
import xyz.mcutils.backend.repository.PlayerCacheRepository;
|
||||
import xyz.mcutils.backend.repository.PlayerNameCacheRepository;
|
||||
import xyz.mcutils.backend.repository.PlayerSkinPartCacheRepository;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalPlayerLookupsMetric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
|
||||
import xyz.mcutils.backend.repository.redis.PlayerCacheRepository;
|
||||
import xyz.mcutils.backend.repository.redis.PlayerNameCacheRepository;
|
||||
import xyz.mcutils.backend.repository.redis.PlayerSkinPartCacheRepository;
|
||||
import xyz.mcutils.backend.service.metric.metrics.UniquePlayerLookupsMetric;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service @Log4j2
|
||||
@Service @Log4j2(topic = "Player Service")
|
||||
public class PlayerService {
|
||||
|
||||
private final MojangService mojangAPIService;
|
||||
@ -65,10 +60,8 @@ public class PlayerService {
|
||||
uuid = usernameToUuid(id).getUniqueId();
|
||||
}
|
||||
|
||||
((TotalPlayerLookupsMetric) metricService.getMetric(TotalPlayerLookupsMetric.class)).increment(); // Increment the total player lookups
|
||||
|
||||
Optional<CachedPlayer> cachedPlayer = playerCacheRepository.findById(uuid);
|
||||
if (cachedPlayer.isPresent() && Config.INSTANCE.isProduction()) { // Return the cached player if it exists
|
||||
if (cachedPlayer.isPresent() && AppConfig.isProduction()) { // Return the cached player if it exists
|
||||
log.info("Player {} is cached", id);
|
||||
return cachedPlayer.get();
|
||||
}
|
||||
@ -90,6 +83,10 @@ public class PlayerService {
|
||||
)
|
||||
);
|
||||
|
||||
// Add the lookup to the unique player lookups metric
|
||||
((UniquePlayerLookupsMetric) metricService.getMetric(UniquePlayerLookupsMetric.class))
|
||||
.addLookup(uuid);
|
||||
|
||||
playerCacheRepository.save(player);
|
||||
player.getCache().setCached(false);
|
||||
return player;
|
||||
@ -108,7 +105,7 @@ public class PlayerService {
|
||||
log.info("Getting UUID from username: {}", username);
|
||||
String id = username.toUpperCase();
|
||||
Optional<CachedPlayerName> cachedPlayerName = playerNameCacheRepository.findById(id);
|
||||
if (cachedPlayerName.isPresent() && Config.INSTANCE.isProduction()) {
|
||||
if (cachedPlayerName.isPresent() && AppConfig.isProduction()) {
|
||||
return cachedPlayerName.get();
|
||||
}
|
||||
try {
|
||||
@ -138,12 +135,10 @@ public class PlayerService {
|
||||
*/
|
||||
public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) {
|
||||
if (size > 512) {
|
||||
log.info("Size {} is too large, setting to 512", size);
|
||||
size = 512;
|
||||
throw new BadRequestException("Size cannot be larger than 512");
|
||||
}
|
||||
if (size < 32) {
|
||||
log.info("Size {} is too small, setting to 32", size);
|
||||
size = 32;
|
||||
throw new BadRequestException("Size cannot be smaller than 32");
|
||||
}
|
||||
|
||||
ISkinPart part = ISkinPart.getByName(partName); // The skin part to get
|
||||
@ -158,7 +153,7 @@ public class PlayerService {
|
||||
Optional<CachedPlayerSkinPart> cache = playerSkinPartCacheRepository.findById(key);
|
||||
|
||||
// The skin part is cached
|
||||
if (cache.isPresent() && Config.INSTANCE.isProduction()) {
|
||||
if (cache.isPresent() && AppConfig.isProduction()) {
|
||||
log.info("Skin part {} for player {} is cached", name, player.getUniqueId());
|
||||
return cache.get();
|
||||
}
|
||||
|
@ -3,20 +3,23 @@ package xyz.mcutils.backend.service;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.mcutils.backend.common.AppConfig;
|
||||
import xyz.mcutils.backend.common.DNSUtils;
|
||||
import xyz.mcutils.backend.common.EnumUtils;
|
||||
import xyz.mcutils.backend.config.Config;
|
||||
import xyz.mcutils.backend.common.ImageUtils;
|
||||
import xyz.mcutils.backend.common.renderer.impl.server.ServerPreviewRenderer;
|
||||
import xyz.mcutils.backend.exception.impl.BadRequestException;
|
||||
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
|
||||
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
||||
import xyz.mcutils.backend.model.cache.CachedServerPreview;
|
||||
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||
import xyz.mcutils.backend.model.dns.impl.ARecord;
|
||||
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
|
||||
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||
import xyz.mcutils.backend.repository.MinecraftServerCacheRepository;
|
||||
import xyz.mcutils.backend.service.metric.Metric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
|
||||
import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository;
|
||||
import xyz.mcutils.backend.repository.redis.ServerPreviewCacheRepository;
|
||||
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
@ -24,19 +27,22 @@ import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service @Log4j2
|
||||
@Service @Log4j2(topic = "Server Service")
|
||||
public class ServerService {
|
||||
private static final String DEFAULT_SERVER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAASFBMVEWwsLBBQUE9PT1JSUlFRUUuLi5MTEyzs7M0NDQ5OTlVVVVQUFAmJia5ubl+fn5zc3PFxcVdXV3AwMCJiYmUlJRmZmbQ0NCjo6OL5p+6AAAFVklEQVRYw+1W67K0KAzkJnIZdRAZ3/9NtzvgXM45dX7st1VbW7XBUVDSdEISRqn/5R+T82/+nsr/XZn/SHm/3x9/ArA/IP8qwPK433d44VubZ/XT6/cJy0L792VZfnDrcRznr86d748u92X5vtaxOe228zcCy+MSMpg/5SwRopsYMv8oigCwngbQhE/rzhwAYMpxnvMvHhgy/8AgByJolzb5pPqEbvtgMBBmtvkbgxKmaaIZ5TyPum6Viue6te241N+s+W6nOlucgjEx6Nay9zZta1XVxejW+Q5ZhhkDS31lgOTegjUBor33CQilbC2GYGy9y9bN8ytevjE4a2stajHDAgAcUkoYwzO6zQi8ZflC+XO0+exiuNa3OQtIJOCk13neUjv7VO7Asu/3LwDFeg37sQtQhy4lAQH6IR9ztca0E3oI5PtDAlJ1tHGplrJ12jjrrXPWYvXsU042Bl/qUr3B9qzPSKaovpvjgglYL2F1x+Zs7gIvpLYuq46wr3H5/RJxyvM6sXOY762oU4YZ3mAz1lpc9O3Y30VJUM/iWhBIib63II/LA4COEMxcSmrH4ddl/wTYe3RIO0vK2VI9wQy6AxRsJpb3AAALvXb6TxvUCYSdOQo5Mh0GySkJc7rB405GUEfzbbl/iFpPoNQVNUQAZG06nkI6RCABRqRA9IimH6Up5Mhybtu2IlewB2Sf6AmQ4ZU9rfBELvyA23Yub6LWWtUBgK3OB79L7FILLDKWd4wpxmMRAMoLQR1ItLoiWUmhFtjptab7LQDgRARliLITLrcBkHNp9VACUH1UDRQEYGuYxzyM9H0mBccQNnCkQ3Q1UHBaO6sNyw0CelEtBGXKSoE+fJWZh5GupyneMIkCOMESAniMAzMreLvuO+pnmBQSp4C+ELCiMSGVLPh7M023SSBAiAA5yPh2m0wigEbWKnw3qDrrscF00cciCATGwNQRAv2YGvyD4Y36QGhqOS4AcABAA88oGvBCRho5H2+UiW6EfyM1L5l8a56rqdvE6lFakc3ScVDOBNBUoFM8c1vgnhAG5VsAqMD6Q9IwwtAkR39iGEQF1ZBxgU+v9UGL6MBQYiTdJllIBtx5y0rixGdAZ1YysbS53TAVy3vf4aabEpt1T0HoB2Eg4Yv5OKNwyHgmNvPKaQAYLG3EIyIqcL6Fj5C2jhXL9EpCdRMROE5nCW3qm1vfR6wYh0HKGG3wY+JgLkUWQ/WMfI8oMvIWMY7aCncNxxpSmHRUCEzDdSR0+dRwIQaMWW1FE0AOGeKkx0OLwYanBK3qfC0BSmIlozkuFcvSkulckoIB2FbHWu0y9gMHsEapMMEoySNUA2RDrduxIqr5POQV2zZ++IBOwVrFO9THrtjU2uWsCMZjxXl88Hmeaz1rPdAqXyJl68F5RTtdvN1aIyYEAMAWJaCMHvon7s23jljlxoKBEgNv6LQ25/rZIQyOdwDO3jLsqE2nbVAil21LxqFpZ2xJ3CFuE33QCo7kfkfO8kpW6gdioxdzZDLOaMMwidzeKD0RxaD7cnHHsu0jVkW5oTwwMGI0lwwA36u2nMY8AKzErLW9JxFiteyzZsAAxY1vPe5Uf68lIDVjV8JZpPfjxbc/QuyRKdAQJaAdIA4tCTht+kQJ1I4nbdjfHxgpTSLyI19pb/iuK7+9YJaZCxEIKj79YZ6uDU8f97878teRN1FzA7OvquSrVKUgk+S6ROpJfA7GpN6RPkx4voshXgu91p7CGHeA+IY8dUUVXwT7PYw12Xsj0Lfh9X4ac9XgKW86cj8bPh8XmyDOD88FLoB+YPXp4YtyB3gBPXu98xeRI2zploVCBQAAAABJRU5ErkJggg==";
|
||||
public static final String DEFAULT_SERVER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAASFBMVEWwsLBBQUE9PT1JSUlFRUUuLi5MTEyzs7M0NDQ5OTlVVVVQUFAmJia5ubl+fn5zc3PFxcVdXV3AwMCJiYmUlJRmZmbQ0NCjo6OL5p+6AAAFVklEQVRYw+1W67K0KAzkJnIZdRAZ3/9NtzvgXM45dX7st1VbW7XBUVDSdEISRqn/5R+T82/+nsr/XZn/SHm/3x9/ArA/IP8qwPK433d44VubZ/XT6/cJy0L792VZfnDrcRznr86d748u92X5vtaxOe228zcCy+MSMpg/5SwRopsYMv8oigCwngbQhE/rzhwAYMpxnvMvHhgy/8AgByJolzb5pPqEbvtgMBBmtvkbgxKmaaIZ5TyPum6Viue6te241N+s+W6nOlucgjEx6Nay9zZta1XVxejW+Q5ZhhkDS31lgOTegjUBor33CQilbC2GYGy9y9bN8ytevjE4a2stajHDAgAcUkoYwzO6zQi8ZflC+XO0+exiuNa3OQtIJOCk13neUjv7VO7Asu/3LwDFeg37sQtQhy4lAQH6IR9ztca0E3oI5PtDAlJ1tHGplrJ12jjrrXPWYvXsU042Bl/qUr3B9qzPSKaovpvjgglYL2F1x+Zs7gIvpLYuq46wr3H5/RJxyvM6sXOY762oU4YZ3mAz1lpc9O3Y30VJUM/iWhBIib63II/LA4COEMxcSmrH4ddl/wTYe3RIO0vK2VI9wQy6AxRsJpb3AAALvXb6TxvUCYSdOQo5Mh0GySkJc7rB405GUEfzbbl/iFpPoNQVNUQAZG06nkI6RCABRqRA9IimH6Up5Mhybtu2IlewB2Sf6AmQ4ZU9rfBELvyA23Yub6LWWtUBgK3OB79L7FILLDKWd4wpxmMRAMoLQR1ItLoiWUmhFtjptab7LQDgRARliLITLrcBkHNp9VACUH1UDRQEYGuYxzyM9H0mBccQNnCkQ3Q1UHBaO6sNyw0CelEtBGXKSoE+fJWZh5GupyneMIkCOMESAniMAzMreLvuO+pnmBQSp4C+ELCiMSGVLPh7M023SSBAiAA5yPh2m0wigEbWKnw3qDrrscF00cciCATGwNQRAv2YGvyD4Y36QGhqOS4AcABAA88oGvBCRho5H2+UiW6EfyM1L5l8a56rqdvE6lFakc3ScVDOBNBUoFM8c1vgnhAG5VsAqMD6Q9IwwtAkR39iGEQF1ZBxgU+v9UGL6MBQYiTdJllIBtx5y0rixGdAZ1YysbS53TAVy3vf4aabEpt1T0HoB2Eg4Yv5OKNwyHgmNvPKaQAYLG3EIyIqcL6Fj5C2jhXL9EpCdRMROE5nCW3qm1vfR6wYh0HKGG3wY+JgLkUWQ/WMfI8oMvIWMY7aCncNxxpSmHRUCEzDdSR0+dRwIQaMWW1FE0AOGeKkx0OLwYanBK3qfC0BSmIlozkuFcvSkulckoIB2FbHWu0y9gMHsEapMMEoySNUA2RDrduxIqr5POQV2zZ++IBOwVrFO9THrtjU2uWsCMZjxXl88Hmeaz1rPdAqXyJl68F5RTtdvN1aIyYEAMAWJaCMHvon7s23jljlxoKBEgNv6LQ25/rZIQyOdwDO3jLsqE2nbVAil21LxqFpZ2xJ3CFuE33QCo7kfkfO8kpW6gdioxdzZDLOaMMwidzeKD0RxaD7cnHHsu0jVkW5oTwwMGI0lwwA36u2nMY8AKzErLW9JxFiteyzZsAAxY1vPe5Uf68lIDVjV8JZpPfjxbc/QuyRKdAQJaAdIA4tCTht+kQJ1I4nbdjfHxgpTSLyI19pb/iuK7+9YJaZCxEIKj79YZ6uDU8f97878teRN1FzA7OvquSrVKUgk+S6ROpJfA7GpN6RPkx4voshXgu91p7CGHeA+IY8dUUVXwT7PYw12Xsj0Lfh9X4ac9XgKW86cj8bPh8XmyDOD88FLoB+YPXp4YtyB3gBPXu98xeRI2zploVCBQAAAABJRU5ErkJggg==";
|
||||
|
||||
private final MojangService mojangService;
|
||||
private final MetricService metricService;
|
||||
private final MinecraftServerCacheRepository serverCacheRepository;
|
||||
private final ServerPreviewCacheRepository serverPreviewCacheRepository;
|
||||
|
||||
@Autowired
|
||||
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository) {
|
||||
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository,
|
||||
ServerPreviewCacheRepository serverPreviewCacheRepository) {
|
||||
this.mojangService = mojangService;
|
||||
this.metricService = metricService;
|
||||
this.serverCacheRepository = serverCacheRepository;
|
||||
this.serverPreviewCacheRepository = serverPreviewCacheRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,11 +72,9 @@ public class ServerService {
|
||||
String key = "%s-%s:%s".formatted(platformName, hostname, port);
|
||||
log.info("Getting server: {}:{}", hostname, port);
|
||||
|
||||
((TotalServerLookupsMetric) metricService.getMetric(TotalServerLookupsMetric.class)).increment(); // Increment the total server lookups
|
||||
|
||||
// Check if the server is cached
|
||||
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
|
||||
if (cached.isPresent() && Config.INSTANCE.isProduction()) {
|
||||
if (cached.isPresent() && AppConfig.isProduction()) {
|
||||
log.info("Server {}:{} is cached", hostname, port);
|
||||
return cached.get();
|
||||
}
|
||||
@ -102,6 +106,10 @@ public class ServerService {
|
||||
((JavaMinecraftServer) server.getServer()).setMojangBlocked(mojangService.isServerBlocked(hostname));
|
||||
}
|
||||
|
||||
// Add the server lookup to the unique server lookups metric
|
||||
((UniqueServerLookupsMetric) metricService.getMetric(UniqueServerLookupsMetric.class))
|
||||
.addLookup("%s-%s:%s".formatted(platformName, hostname, port));
|
||||
|
||||
log.info("Found server: {}:{}", hostname, port);
|
||||
serverCacheRepository.save(server);
|
||||
server.getCache().setCached(false);
|
||||
@ -130,4 +138,39 @@ public class ServerService {
|
||||
}
|
||||
return Base64.getDecoder().decode(icon); // Return the decoded favicon
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server list preview image.
|
||||
*
|
||||
* @param cachedServer the server to get the preview of
|
||||
* @param platform the platform of the server
|
||||
* @param size the size of the preview
|
||||
* @return the server preview
|
||||
*/
|
||||
public byte[] getServerPreview(CachedMinecraftServer cachedServer, String platform, int size) {
|
||||
if (size > 2048) {
|
||||
throw new BadRequestException("Size cannot be greater than 2048");
|
||||
}
|
||||
if (size < 256) {
|
||||
throw new BadRequestException("Size cannot be smaller than 256");
|
||||
}
|
||||
MinecraftServer server = cachedServer.getServer();
|
||||
log.info("Getting preview for server: {}:{} (size {})", server.getHostname(), server.getPort(), size);
|
||||
String key = "%s-%s:%s".formatted(platform, server.getHostname(), server.getPort());
|
||||
|
||||
// Check if the server preview is cached
|
||||
Optional<CachedServerPreview> cached = serverPreviewCacheRepository.findById(key);
|
||||
if (cached.isPresent() && AppConfig.isProduction()) {
|
||||
log.info("Server preview for {}:{} is cached", server.getHostname(), server.getPort());
|
||||
return cached.get().getBytes();
|
||||
}
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
byte[] preview = ImageUtils.imageToBytes(ServerPreviewRenderer.INSTANCE.render(server, size));
|
||||
log.info("Took {}ms to render preview for server: {}:{}", System.currentTimeMillis() - start, server.getHostname(), server.getPort());
|
||||
|
||||
CachedServerPreview serverPreview = new CachedServerPreview(key, preview);
|
||||
serverPreviewCacheRepository.save(serverPreview);
|
||||
return preview;
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.annotation.Transient;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter @Setter @ToString
|
||||
@RedisHash(value = "metric")
|
||||
@Document("metrics")
|
||||
public abstract class Metric<T> {
|
||||
/**
|
||||
* The id of the metric.
|
||||
@ -27,8 +28,8 @@ public abstract class Metric<T> {
|
||||
* Should this metric be collected
|
||||
* before pushing to Influx?
|
||||
*/
|
||||
@JsonIgnore
|
||||
private transient boolean collector;
|
||||
@Transient @JsonIgnore
|
||||
private boolean collector;
|
||||
|
||||
/**
|
||||
* Collects the metric.
|
||||
|
@ -1,7 +1,7 @@
|
||||
package xyz.mcutils.backend.service.metric.metrics;
|
||||
|
||||
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
|
||||
import xyz.mcutils.backend.websocket.MetricsWebSocketHandler;
|
||||
import xyz.mcutils.backend.websocket.WebSocketManager;
|
||||
|
||||
public class ConnectedSocketsMetric extends IntegerMetric {
|
||||
|
||||
@ -16,6 +16,6 @@ public class ConnectedSocketsMetric extends IntegerMetric {
|
||||
|
||||
@Override
|
||||
public void collect() {
|
||||
setValue(MetricsWebSocketHandler.SESSIONS.size());
|
||||
setValue(WebSocketManager.getTotalConnections());
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package xyz.mcutils.backend.service.metric.metrics;
|
||||
|
||||
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
|
||||
|
||||
public class TotalPlayerLookupsMetric extends IntegerMetric {
|
||||
|
||||
public TotalPlayerLookupsMetric() {
|
||||
super("total_player_lookups");
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package xyz.mcutils.backend.service.metric.metrics;
|
||||
|
||||
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
|
||||
|
||||
public class TotalServerLookupsMetric extends IntegerMetric {
|
||||
|
||||
public TotalServerLookupsMetric() {
|
||||
super("total_server_lookups");
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package xyz.mcutils.backend.service.metric.metrics;
|
||||
|
||||
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UniquePlayerLookupsMetric extends IntegerMetric {
|
||||
private List<String> uniqueLookups = new ArrayList<>();
|
||||
|
||||
public UniquePlayerLookupsMetric() {
|
||||
super("unique_player_lookups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollector() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a lookup to the list of unique lookups.
|
||||
*
|
||||
* @param uuid the query that was used to look up a player
|
||||
*/
|
||||
public void addLookup(UUID uuid) {
|
||||
if (!uniqueLookups.contains(uuid.toString())) {
|
||||
uniqueLookups.add(uuid.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect() {
|
||||
setValue(uniqueLookups.size());
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package xyz.mcutils.backend.service.metric.metrics;
|
||||
|
||||
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class UniqueServerLookupsMetric extends IntegerMetric {
|
||||
private List<String> uniqueLookups = new ArrayList<>();
|
||||
|
||||
public UniqueServerLookupsMetric() {
|
||||
super("unique_server_lookups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollector() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a lookup to the list of unique lookups.
|
||||
*
|
||||
* @param hostname the query that was used to look up a player
|
||||
*/
|
||||
public void addLookup(String hostname) {
|
||||
hostname = hostname.toLowerCase();
|
||||
if (!uniqueLookups.contains(hostname)) {
|
||||
uniqueLookups.add(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect() {
|
||||
setValue(uniqueLookups.size());
|
||||
}
|
||||
}
|
@ -7,10 +7,15 @@ import xyz.mcutils.backend.exception.impl.BadRequestException;
|
||||
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
|
||||
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||
import xyz.mcutils.backend.model.server.BedrockMinecraftServer;
|
||||
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||
import xyz.mcutils.backend.service.MaxMindService;
|
||||
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* The {@link MinecraftServerPinger} for pinging
|
||||
@ -50,19 +55,19 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
|
||||
unconnectedPong.process(socket);
|
||||
String response = unconnectedPong.getResponse();
|
||||
if (response == null) { // No pong response
|
||||
throw new ResourceNotFoundException("Server didn't respond to ping");
|
||||
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
|
||||
}
|
||||
return BedrockMinecraftServer.create(hostname, ip, port, records, response); // Return the server
|
||||
return BedrockMinecraftServer.create(hostname, ip, port, records,
|
||||
MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(ip)), response); // Return the server
|
||||
} catch (IOException ex ) {
|
||||
if (ex instanceof UnknownHostException) {
|
||||
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
|
||||
throw new BadRequestException("Unknown hostname '%s'".formatted(hostname));
|
||||
} else if (ex instanceof SocketTimeoutException) {
|
||||
throw new ResourceNotFoundException(ex);
|
||||
} else if (ex instanceof SocketException) {
|
||||
throw new BadRequestException("An error occurred pinging %s:%s".formatted(hostname, port));
|
||||
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
|
||||
} else {
|
||||
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
|
||||
throw new BadRequestException("An error occurred pinging '%s:%s'".formatted(hostname, port));
|
||||
}
|
||||
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -3,14 +3,15 @@ package xyz.mcutils.backend.service.pinger.impl;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import xyz.mcutils.backend.Main;
|
||||
import xyz.mcutils.backend.common.JavaMinecraftVersion;
|
||||
import xyz.mcutils.backend.common.ServerUtils;
|
||||
import xyz.mcutils.backend.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
|
||||
import xyz.mcutils.backend.common.packet.impl.java.JavaPacketStatusInStart;
|
||||
import xyz.mcutils.backend.exception.impl.BadRequestException;
|
||||
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
|
||||
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||
import xyz.mcutils.backend.model.token.JavaServerStatusToken;
|
||||
import xyz.mcutils.backend.service.MaxMindService;
|
||||
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
@ -25,6 +26,13 @@ import java.net.*;
|
||||
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
|
||||
private static final int TIMEOUT = 1500; // The timeout for the socket
|
||||
|
||||
/**
|
||||
* Ping the server with the given hostname and port.
|
||||
*
|
||||
* @param hostname the hostname of the server
|
||||
* @param port the port of the server
|
||||
* @return the server that was pinged
|
||||
*/
|
||||
@Override
|
||||
public JavaMinecraftServer ping(String hostname, String ip, int port, DNSRecord[] records) {
|
||||
log.info("Pinging {}:{}...", hostname, port);
|
||||
@ -44,16 +52,18 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
|
||||
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
||||
packetStatusInStart.process(inputStream, outputStream);
|
||||
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
|
||||
return JavaMinecraftServer.create(hostname, ip, port, records, token);
|
||||
return JavaMinecraftServer.create(hostname, ip, port, records,
|
||||
MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(ip)), token);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (ex instanceof UnknownHostException) {
|
||||
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
|
||||
} else if (ex instanceof ConnectException || ex instanceof SocketTimeoutException) {
|
||||
throw new ResourceNotFoundException(ex);
|
||||
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
|
||||
} else {
|
||||
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
|
||||
throw new BadRequestException("An error occurred pinging '%s:%s'".formatted(hostname, port));
|
||||
}
|
||||
log.error("An error occurred pinging %s".formatted(ServerUtils.getAddress(hostname, port)), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package xyz.mcutils.backend.websocket;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
import xyz.mcutils.backend.Main;
|
||||
import xyz.mcutils.backend.common.Timer;
|
||||
import xyz.mcutils.backend.model.metric.WebsocketMetrics;
|
||||
import xyz.mcutils.backend.service.MetricService;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalPlayerLookupsMetric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Log4j2(topic = "WebSocket/Metrics")
|
||||
public class MetricsWebSocketHandler extends TextWebSocketHandler {
|
||||
private final long interval = TimeUnit.SECONDS.toMillis(5);
|
||||
public static final List<WebSocketSession> SESSIONS = new ArrayList<>();
|
||||
|
||||
private final MetricService metricService;
|
||||
|
||||
public MetricsWebSocketHandler(MetricService metricService) {
|
||||
this.metricService = metricService;
|
||||
Timer.scheduleRepeating(() -> {
|
||||
for (WebSocketSession session : SESSIONS) {
|
||||
sendMetrics(session);
|
||||
}
|
||||
}, interval, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the metrics to the client.
|
||||
*
|
||||
* @param session the session to send the metrics to
|
||||
*/
|
||||
private void sendMetrics(WebSocketSession session) {
|
||||
try {
|
||||
WebsocketMetrics metrics = new WebsocketMetrics(Map.of(
|
||||
"totalRequests", metricService.getMetric(TotalRequestsMetric.class).getValue(),
|
||||
"totalServerLookups", metricService.getMetric(TotalServerLookupsMetric.class).getValue(),
|
||||
"totalPlayerLookups", metricService.getMetric(TotalPlayerLookupsMetric.class).getValue()
|
||||
));
|
||||
|
||||
session.sendMessage(new TextMessage(Main.GSON.toJson(metrics)));
|
||||
} catch (Exception e) {
|
||||
log.error("An error occurred while sending metrics to the client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
log.info("WebSocket connection established with session id: {}", session.getId());
|
||||
|
||||
sendMetrics(session);
|
||||
SESSIONS.add(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, @NotNull CloseStatus status) {
|
||||
log.info("WebSocket connection closed with session id: {}", session.getId());
|
||||
|
||||
SESSIONS.remove(session);
|
||||
}
|
||||
}
|
62
src/main/java/xyz/mcutils/backend/websocket/WebSocket.java
Normal file
62
src/main/java/xyz/mcutils/backend/websocket/WebSocket.java
Normal file
@ -0,0 +1,62 @@
|
||||
package xyz.mcutils.backend.websocket;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor @Getter @Log4j2(topic = "WebSocket")
|
||||
public abstract class WebSocket extends TextWebSocketHandler {
|
||||
|
||||
/**
|
||||
* The sessions that are connected to the WebSocket.
|
||||
*/
|
||||
private final List<WebSocketSession> sessions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The path of the WebSocket.
|
||||
* <p>
|
||||
* Example: /websocket/metrics
|
||||
* </p>
|
||||
*/
|
||||
public final String path;
|
||||
|
||||
/**
|
||||
* Sends a message to the client.
|
||||
*
|
||||
* @param session the session to send the message to
|
||||
* @param message the message to send
|
||||
* @throws IOException if an error occurs while sending the message
|
||||
*/
|
||||
public void sendMessage(WebSocketSession session, String message) throws IOException {
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a session connects to the WebSocket.
|
||||
*
|
||||
* @param session the session that connected
|
||||
*/
|
||||
abstract public void onSessionConnect(WebSocketSession session);
|
||||
|
||||
@Override
|
||||
public final void afterConnectionEstablished(@NotNull WebSocketSession session) {
|
||||
this.sessions.add(session);
|
||||
log.info("Connection established: {}", session.getId());
|
||||
this.onSessionConnect(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus status) {
|
||||
this.sessions.remove(session);
|
||||
log.info("Connection closed: {}", session.getId());
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package xyz.mcutils.backend.websocket;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import xyz.mcutils.backend.service.MetricService;
|
||||
import xyz.mcutils.backend.websocket.impl.MetricsWebSocket;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketManager implements WebSocketConfigurer {
|
||||
private static final List<WebSocket> WEB_SOCKETS = new ArrayList<>();
|
||||
|
||||
private final MetricService metricService;
|
||||
|
||||
@Autowired
|
||||
public WebSocketManager(MetricService metricService) {
|
||||
this.metricService = metricService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(@NotNull WebSocketHandlerRegistry registry) {
|
||||
registerWebSocket(registry, new MetricsWebSocket(metricService));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a WebSocket.
|
||||
*
|
||||
* @param registry the registry to register the WebSocket on
|
||||
* @param webSocket the WebSocket to register
|
||||
*/
|
||||
private void registerWebSocket(WebSocketHandlerRegistry registry, WebSocket webSocket) {
|
||||
registry.addHandler(webSocket, webSocket.getPath()).setAllowedOrigins("*");
|
||||
WEB_SOCKETS.add(webSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total amount of connections.
|
||||
*
|
||||
* @return the total amount of connections
|
||||
*/
|
||||
public static int getTotalConnections() {
|
||||
return WEB_SOCKETS.stream().mapToInt(webSocket -> webSocket.getSessions().size()).sum();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package xyz.mcutils.backend.websocket.impl;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import xyz.mcutils.backend.Main;
|
||||
import xyz.mcutils.backend.common.Timer;
|
||||
import xyz.mcutils.backend.service.MetricService;
|
||||
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.UniquePlayerLookupsMetric;
|
||||
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
|
||||
import xyz.mcutils.backend.websocket.WebSocket;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Log4j2(topic = "WebSocket/Metrics")
|
||||
public class MetricsWebSocket extends WebSocket {
|
||||
private final long interval = TimeUnit.SECONDS.toMillis(5);
|
||||
private final MetricService metricService;
|
||||
|
||||
public MetricsWebSocket(MetricService metricService) {
|
||||
super("/websocket/metrics");
|
||||
this.metricService = metricService;
|
||||
|
||||
Timer.scheduleRepeating(() -> {
|
||||
for (WebSocketSession session : this.getSessions()) {
|
||||
sendMetrics(session);
|
||||
}
|
||||
}, interval, interval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionConnect(WebSocketSession session) {
|
||||
sendMetrics(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the metrics to the client.
|
||||
*
|
||||
* @param session the session to send the metrics to
|
||||
*/
|
||||
private void sendMetrics(WebSocketSession session) {
|
||||
try {
|
||||
this.sendMessage(session, Main.GSON.toJson(Map.of(
|
||||
"totalRequests", metricService.getMetric(TotalRequestsMetric.class).getValue(),
|
||||
"uniqueServerLookups", metricService.getMetric(UniqueServerLookupsMetric.class).getValue(),
|
||||
"uniquePlayerLookups", metricService.getMetric(UniquePlayerLookupsMetric.class).getValue()
|
||||
)));
|
||||
} catch (Exception e) {
|
||||
log.error("An error occurred while sending metrics to the client", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,17 +17,23 @@ spring:
|
||||
database: 1
|
||||
auth: "" # Leave blank for no auth
|
||||
|
||||
# Disable default metrics
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
exclude:
|
||||
- "*"
|
||||
influx:
|
||||
metrics:
|
||||
export:
|
||||
enabled: false
|
||||
# MongoDB - This is used for general data storage
|
||||
mongodb:
|
||||
uri: mongodb://localhost:27017
|
||||
database: test
|
||||
port: 27017
|
||||
|
||||
# Sentry Configuration
|
||||
sentry:
|
||||
dsn: ""
|
||||
|
||||
# The URL of the API
|
||||
public-url: http://localhost
|
||||
|
||||
# MaxMind Configuration
|
||||
# This is used for IP Geolocation
|
||||
maxmind:
|
||||
license: ""
|
||||
|
||||
# InfluxDB Configuration
|
||||
influx:
|
||||
@ -36,4 +42,22 @@ influx:
|
||||
org: org
|
||||
bucket: bucket
|
||||
|
||||
public-url: http://localhost
|
||||
management:
|
||||
# Disable all actuator endpoints
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
exclude:
|
||||
- "*"
|
||||
# Disable default metrics
|
||||
influx:
|
||||
metrics:
|
||||
export:
|
||||
enabled: false
|
||||
|
||||
# Set the embedded MongoDB version
|
||||
de:
|
||||
flapdoodle:
|
||||
mongodb:
|
||||
embedded:
|
||||
version: 7.0.8
|
BIN
src/main/resources/fonts/minecraft-font.ttf
Normal file
BIN
src/main/resources/fonts/minecraft-font.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/icons/ping.png
Normal file
BIN
src/main/resources/icons/ping.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 B |
BIN
src/main/resources/icons/server_background.png
Normal file
BIN
src/main/resources/icons/server_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 593 B |
@ -1,4 +1,4 @@
|
||||
package cc.fascinated.config;
|
||||
package xyz.mcutils.backend.test.config;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
@ -16,7 +16,8 @@ import java.io.IOException;
|
||||
*/
|
||||
@TestConfiguration
|
||||
public class TestRedisConfig {
|
||||
@NonNull private final RedisServer server;
|
||||
@NonNull
|
||||
private final RedisServer server;
|
||||
|
||||
public TestRedisConfig() throws IOException {
|
||||
server = new RedisServer(); // Construct the mock server
|
@ -1,18 +1,18 @@
|
||||
package xyz.mcutils.backend.tests;
|
||||
package xyz.mcutils.backend.test.tests;
|
||||
|
||||
import cc.fascinated.config.TestRedisConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import xyz.mcutils.backend.test.config.TestRedisConfig;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(classes = { TestRedisConfig.class })
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes = TestRedisConfig.class)
|
||||
class MojangControllerTests {
|
||||
|
||||
@Autowired
|
@ -1,20 +1,22 @@
|
||||
package xyz.mcutils.backend.tests;
|
||||
package xyz.mcutils.backend.test.tests;
|
||||
|
||||
import cc.fascinated.config.TestRedisConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||
import xyz.mcutils.backend.test.config.TestRedisConfig;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(classes = { TestRedisConfig.class })
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes = TestRedisConfig.class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class PlayerControllerTests {
|
||||
|
||||
private final String testPlayerUuid = "eeab5f8a-18dd-4d58-af78-2b3c4543da48";
|
@ -1,19 +1,21 @@
|
||||
package xyz.mcutils.backend.tests;
|
||||
package xyz.mcutils.backend.test.tests;
|
||||
|
||||
import cc.fascinated.config.TestRedisConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import xyz.mcutils.backend.test.config.TestRedisConfig;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(classes = { TestRedisConfig.class })
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes = TestRedisConfig.class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ServerControllerTests {
|
||||
|
||||
private final String testServer = "play.hypixel.net";
|
||||
@ -54,4 +56,11 @@ class ServerControllerTests {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.blocked").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ensureServerPreviewLookupSuccess() throws Exception {
|
||||
mockMvc.perform(get("/server/java/preview/" + testServer)
|
||||
.contentType(MediaType.IMAGE_PNG))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user