1 Commits

Author SHA1 Message Date
Renovate Bot
3f1cc9887e chore(deps): update nextjs monorepo to v14.2.2 2024-04-18 04:02:23 +00:00
235 changed files with 1257 additions and 9535 deletions

View File

@ -3,7 +3,7 @@ name: Deploy API
on:
push:
branches: ["master"]
paths: [".gitea/workflows/deploy-api.yml", "API/**", "!API/README.md"]
paths: [".gitea/workflows/deploy-api.yml", "API/**"]
jobs:
docker:

View File

@ -1,38 +0,0 @@
name: Deploy Discord Bot
on:
push:
branches: ["master"]
paths: [".gitea/workflows/deploy-bot.yml", "DiscordBot/**", "!DiscordBot/README.md"]
jobs:
docker:
strategy:
matrix:
java-version: ["17"]
maven-version: ["3.8.5"]
runs-on: "ubuntu-latest"
defaults:
run:
working-directory: "./DiscordBot"
# Steps to run
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@v4
# Setup Java and Maven
- name: Set up JDK and Maven
uses: s4u/setup-maven-action@v1.12.0
with:
java-version: ${{ matrix.java-version }}
distribution: "zulu"
maven-version: ${{ matrix.maven-version }}
# Deploy to Dokku
- name: Deploy to Dokku
uses: dokku/github-action@master
with:
git_remote_url: "ssh://dokku@10.10.3.28:22/restfulmc-bot"
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}

View File

@ -3,7 +3,7 @@ name: Deploy Frontend
on:
push:
branches: ["master"]
paths: [".gitea/workflows/deploy-frontend.yml", "Frontend/**", "!Frontend/README.md"]
paths: [".gitea/workflows/deploy-frontend.yml", "Frontend/**"]
jobs:
docker:

View File

@ -1,53 +0,0 @@
name: Publish Java SDK
on:
push:
branches: ["master"]
paths:
[".gitea/workflows/publish-java-sdk.yml", "Java-SDK/**", "!Java-SDK/README.md"]
jobs:
docker:
strategy:
matrix:
java-version: ["17"]
maven-version: ["3.8.5"]
runs-on: "ubuntu-latest"
defaults:
run:
working-directory: "./Java-SDK"
# Steps to run
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@v4
# Setup Java and Maven
- name: Set up JDK and Maven
uses: s4u/setup-maven-action@v1.12.0
with:
java-version: ${{ matrix.java-version }}
distribution: "zulu"
maven-version: ${{ matrix.maven-version }}
# Configure Maven settings
- name: Maven Settings
uses: s4u/maven-settings-action@v3.0.0
with:
servers: |-
[
{
"id": "rainnny-repo-public",
"username": "${{ secrets.PRIVATE_MAVEN_USER }}",
"password": "${{ secrets.PRIVATE_MAVEN_PASS }}"
}
]
# Build the project
- name: Maven Build
run: mvn clean package -e -T6C
# Publish to Maven
- name: Publish to Maven
run: mvn deploy -Pgen-javadocs -B -Dstyle.color=always --update-snapshots -e -T6C

View File

@ -3,8 +3,7 @@ name: Publish JS SDK
on:
push:
branches: ["master"]
paths:
[".gitea/workflows/publish-js-sdk.yml", "JS-SDK/**", "!JS-SDK/README.md"]
paths: [".gitea/workflows/publish-js-sdk.yml", "JS-SDK/**"]
jobs:
docker:

View File

@ -6,12 +6,12 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<version>3.2.4</version>
<relativePath/>
</parent>
<!-- Project Details -->
<groupId>cc.restfulmc</groupId>
<groupId>me.braydon</groupId>
<artifactId>RESTfulMC</artifactId>
<version>1.0.0</version>
<description>A simple, yet useful RESTful API for Minecraft utilizing Springboot.</description>
@ -86,7 +86,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -107,12 +107,6 @@
<version>1.20-R0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
<version>4.9.2</version>
<scope>compile</scope>
</dependency>
<!-- DNS Lookup -->
<dependency>
@ -122,14 +116,6 @@
<scope>compile</scope>
</dependency>
<!-- GeoIP -->
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>4.2.0</version>
<scope>compile</scope>
</dependency>
<!-- SwaggerUI -->
<dependency>
<groupId>org.springdoc</groupId>
@ -138,14 +124,6 @@
<scope>compile</scope>
</dependency>
<!-- Sentry -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.8.0</version>
<scope>compile</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -1,101 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet;
import java.util.ArrayList;
/**
* @author Braydon
*/
public abstract class JavaQueryPacket extends UDPPacket {
protected static byte[] MAGIC = { (byte) 0xFE, (byte) 0xFD };
protected final byte[] padArrayEnd(byte[] array, int amount) {
byte[] result = new byte[array.length + amount];
System.arraycopy(array, 0, result, 0, array.length);
for (int i = array.length; i < result.length; i++) {
result[i] = 0;
}
return result;
}
protected final byte[] intToBytes(int input) {
return new byte[] {
(byte) (input >>> 24 & 0xFF),
(byte) (input >>> 16 & 0xFF),
(byte) (input >>> 8 & 0xFF),
(byte) (input & 0xFF)
};
}
protected final byte[] trim(byte[] arr) {
int begin = 0, end = arr.length;
for (int i = 0; i < arr.length; i++) { // find the first non-null byte{
if (arr[i] != 0) {
begin = i;
break;
}
}
for (int i = arr.length - 1; i >= 0; i--) { //find the last non-null byte
if (arr[i] != 0) {
end = i;
break;
}
}
return subarray(arr, begin, end);
}
protected final byte[] subarray(byte[] in, int a, int b) {
if (b - a > in.length) {
return in;
}
byte[] out = new byte[(b - a) + 1];
if (b + 1 - a >= 0) {
System.arraycopy(in, a, out, 0, b + 1 - a);
}
return out;
}
protected final byte[][] split(byte[] input) {
ArrayList<byte[]> temp = new ArrayList<>();
int index_cache = 0;
for (int i = 0; i < input.length; i++) {
if (input[i] == 0x00) {
byte[] b = subarray(input, index_cache, i - 1);
temp.add(b);
index_cache = i + 1;//note, this is the index *after* the null byte
}
}
//get the remaining part
if (index_cache != 0) { //prevent duplication if there are no null bytes
byte[] b = subarray(input, index_cache, input.length - 1);
temp.add(b);
}
byte[][] output = new byte[temp.size()][input.length];
for (int i = 0; i < temp.size(); i++) {
output[i] = temp.get(i);
}
return output;
}
}

View File

@ -1,75 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.udp;
import cc.restfulmc.api.common.packet.JavaQueryPacket;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* This packet is sent by the client to the server to request the
* full stats of the server. The server will respond with a payload
* containing the server's stats.
*
* @author Braydon
* @see <a href="https://wiki.vg/Query#Request_3">Query Protocol Docs</a>
*/
@AllArgsConstructor
public final class JavaQueryFullStatRequestPacket extends JavaQueryPacket {
private static final int ID = 0; // The ID of the packet
/**
* The response from the {@link JavaQueryHandshakeRequestPacket}.
*/
private final byte[] handshakeResponse;
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
try (
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(1460);
DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream)
) {
dataOutputStream.write(MAGIC);
dataOutputStream.write(ID); // Packet ID
dataOutputStream.writeInt(1); // Session ID
dataOutputStream.write(padArrayEnd(handshakeResponse, 4)); // The handshake response payload
// Send the packet
byte[] bytes = arrayOutputStream.toByteArray();
socket.send(new DatagramPacket(bytes, bytes.length));
}
}
}

View File

@ -1,79 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.udp;
import cc.restfulmc.api.common.packet.JavaQueryPacket;
import lombok.Getter;
import lombok.NonNull;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.HashMap;
import java.util.Map;
/**
* This packet is sent by the server to the client in
* response to the {@link JavaQueryFullStatRequestPacket}.
*
* @author Braydon
* @see <a href="https://wiki.vg/Query#Response_3">Query Protocol Docs</a>
*/
@Getter
public final class JavaQueryFullStatResponsePacket extends JavaQueryPacket {
/**
* The response from the server, null if none.
*/
private Map<String, String> response;
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
// Handle receiving of the packet
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
socket.receive(receivePacket);
// Construct a response from the received packet.
Map<String, String> response = new HashMap<>();
String previousEntry = null;
for (byte[] bytes : split(trim(receivePacket.getData()))) {
String entry = new String(bytes); // The entry
if (previousEntry != null) {
response.put(previousEntry, entry);
previousEntry = null;
continue;
}
previousEntry = entry;
}
this.response = response;
}
}

View File

@ -1,70 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.udp;
import cc.restfulmc.api.common.packet.JavaQueryPacket;
import lombok.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* This packet is sent by the client to the
* server to request a handshake. This will
* then allow us to send further packets such
* as {@link JavaQueryFullStatRequestPacket}.
*
* @author Braydon
* @see <a href="https://wiki.vg/Query#Request">Query Protocol Docs</a>
*/
public final class JavaQueryHandshakeRequestPacket extends JavaQueryPacket {
private static final int ID = 9; // The ID of the packet
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
try (
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(1460);
DataOutputStream dataOutputStream = new DataOutputStream(arrayOutputStream)
) {
dataOutputStream.write(MAGIC);
dataOutputStream.write(ID); // Packet ID
dataOutputStream.writeInt(1); // Session ID
dataOutputStream.write(new byte[] {}); // No payload data
// Send the packet
byte[] bytes = arrayOutputStream.toByteArray();
bytes = padArrayEnd(bytes, 11 - bytes.length);
socket.send(new DatagramPacket(bytes, bytes.length));
}
}
}

View File

@ -1,64 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.udp;
import cc.restfulmc.api.common.packet.JavaQueryPacket;
import lombok.Getter;
import lombok.NonNull;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* This packet is sent by the server to the client in
* response to the {@link JavaQueryHandshakeRequestPacket}.
*
* @author Braydon
* @see <a href="https://wiki.vg/Query#Response">Query Protocol Docs</a>
*/
@Getter
public final class JavaQueryHandshakeResponsePacket extends JavaQueryPacket {
/**
* The response from the server.
*/
private byte[] response;
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
// Handle receiving of the packet
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
socket.receive(receivePacket);
// Set the response to the integer value of the received data
response = intToBytes(Integer.parseInt(new String(receivePacket.getData()).trim()));
}
}

View File

@ -1,76 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.token;
import cc.restfulmc.api.model.server.JavaMinecraftServer;
import lombok.*;
import java.util.HashMap;
import java.util.Map;
/**
* A token representing the response from
* sending a challenge request via UDP to
* a {@link JavaMinecraftServer} using the
* query.
*
* @author Braydon
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString
public final class JavaServerChallengeStatusToken {
/**
* The map (world) of this server.
*/
@NonNull private final String map;
/**
* The software of this server.
*/
@NonNull private final String software;
/**
* The plugins of this server.
*/
private final Map<String, String> plugins;
/**
* Create a new challenge token
* from the given raw data.
*
* @param rawData the raw data
* @return the challenge token
*/
@NonNull
public static JavaServerChallengeStatusToken create(@NonNull Map<String, String> rawData) {
String[] splitPlugins = rawData.get("plugins").split(": ");
String software = splitPlugins[0]; // The server software
Map<String, String> plugins = new HashMap<>();
for (String plugin : splitPlugins[1].split("; ")) {
String[] split = plugin.split(" ");
plugins.put(split[0], split[1]);
}
return new JavaServerChallengeStatusToken(rawData.get("map"), software, plugins);
}
}

View File

@ -1,220 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.service;
import com.maxmind.db.CHMCache;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.AddressNotFoundException;
import com.maxmind.geoip2.model.CityResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.*;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.InetAddress;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
/**
* @author Braydon
*/
@Service @Log4j2(topic = "MaxMind")
public final class MaxMindService {
/**
* The directory to store databases.
*/
private static final File DATABASES_DIRECTORY = new File("databases");
/**
* The endpoint to download database files from.
*/
private static final String DATABASE_DOWNLOAD_ENDPOINT = "https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz";
@Value("${maxmind.license}")
private String license;
/**
* The currently loaded databases.
*/
private final Map<Database, DatabaseReader> databases = new HashMap<>();
@PostConstruct
public void onInitialize() {
// Load the databases
if (!license.equals("CHANGE_ME")) {
loadDatabases();
}
}
/**
* Load the databases.
*/
@SneakyThrows
private void loadDatabases() {
log.info("Loading databases...");
// Create the directory if it doesn't exist
if (!DATABASES_DIRECTORY.exists()) {
DATABASES_DIRECTORY.mkdirs();
}
// Download missing databases
for (Database database : Database.values()) {
File databaseFile = new File(DATABASES_DIRECTORY, database.getEdition() + ".mmdb");
if (!databaseFile.exists()) { // Doesn't exist, download it
downloadDatabase(database, databaseFile);
}
// Load the database and store it
databases.put(database, new DatabaseReader.Builder(databaseFile)
.withCache(new CHMCache()) // Enable caching
.build()
);
log.info("Loaded database {}", database.getEdition());
}
log.info("Loaded {} database(s)", databases.size());
}
/**
* Lookup a city by the given address.
*
* @param address the address
* @return the city response, null if none
*/
@SneakyThrows
public CityResponse lookupCity(@NonNull InetAddress address) {
DatabaseReader database = getDatabase(Database.CITY);
try {
return database == null ? null : database.city(address);
} catch (AddressNotFoundException ignored) {
// Safely ignore this and return null instead
return null;
}
}
/**
* Download the required files
* for the given database.
*
* @param database the database to download
* @param databaseFile the file for the database
*/
@SneakyThrows
private void downloadDatabase(@NonNull Database database, @NonNull File databaseFile) {
File downloadedFile = new File(DATABASES_DIRECTORY, database.getEdition() + ".tar.gz"); // The downloaded file
// Download the database if required
if (!downloadedFile.exists()) {
log.info("Downloading database {}...", database.getEdition());
long before = System.currentTimeMillis();
try (
BufferedInputStream inputStream = new BufferedInputStream(new URL(DATABASE_DOWNLOAD_ENDPOINT.formatted(database.getEdition(), license)).openStream());
FileOutputStream fileOutputStream = new FileOutputStream(downloadedFile)
) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
}
log.info("Downloaded database {} in {}ms", database.getEdition(), System.currentTimeMillis() - before);
}
// Extract the database once downloaded
log.info("Extracting database {}...", database.getEdition());
TarGZipUnArchiver archiver = new TarGZipUnArchiver();
archiver.setSourceFile(downloadedFile);
archiver.setDestDirectory(DATABASES_DIRECTORY);
archiver.extract();
log.info("Extracted database {}", database.getEdition());
// Locate the database file in the extracted directory
File[] files = DATABASES_DIRECTORY.listFiles();
assert files != null; // Ensure files is present
dirLoop: for (File directory : files) {
if (!directory.isDirectory() || !directory.getName().startsWith(database.getEdition())) {
continue;
}
File[] downloadedFiles = directory.listFiles();
assert downloadedFiles != null; // Ensures downloaded files is present
// Find the file for the database, move it to the
// correct directory, and delete the downloaded contents
for (File file : downloadedFiles) {
if (file.isFile() && file.getName().equals(databaseFile.getName())) {
Files.move(file.toPath(), databaseFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// Delete the downloaded contents
FileUtils.deleteDirectory(directory);
FileUtils.deleteQuietly(downloadedFile);
break dirLoop; // We're done here
}
}
}
}
/**
* Get the reader for the given database.
*
* @param database the database to get
* @return the database reader, null if none
*/
public DatabaseReader getDatabase(@NonNull Database database) {
return databases.get(database);
}
/**
* Cleanup when the app is destroyed.
*/
@PreDestroy @SneakyThrows
public void cleanup() {
for (DatabaseReader database : databases.values()) {
database.close();
}
databases.clear();
}
/**
* A database for MaxMind.
*/
@AllArgsConstructor @Getter @ToString
public enum Database {
CITY("GeoLite2-City");
/**
* The edition of this database.
*/
@NonNull private final String edition;
}
}

View File

@ -1,177 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.service.pinger.impl;
import cc.restfulmc.api.common.JavaMinecraftVersion;
import cc.restfulmc.api.common.packet.impl.java.tcp.JavaHandshakingInSetProtocolPacket;
import cc.restfulmc.api.common.packet.impl.java.tcp.JavaStatusInStartPacket;
import cc.restfulmc.api.common.packet.impl.java.udp.JavaQueryFullStatRequestPacket;
import cc.restfulmc.api.common.packet.impl.java.udp.JavaQueryFullStatResponsePacket;
import cc.restfulmc.api.common.packet.impl.java.udp.JavaQueryHandshakeRequestPacket;
import cc.restfulmc.api.common.packet.impl.java.udp.JavaQueryHandshakeResponsePacket;
import cc.restfulmc.api.config.AppConfig;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.exception.impl.ResourceNotFoundException;
import cc.restfulmc.api.model.dns.DNSRecord;
import cc.restfulmc.api.model.server.JavaMinecraftServer;
import cc.restfulmc.api.model.token.JavaServerChallengeStatusToken;
import cc.restfulmc.api.model.token.JavaServerStatusToken;
import cc.restfulmc.api.service.pinger.MinecraftServerPinger;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.*;
/**
* The {@link MinecraftServerPinger} for pinging
* {@link JavaMinecraftServer}'s over TCP/UDP.
*
* @author Braydon
*/
@Log4j2(topic = "Java MC Server Pinger")
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
private static final int TIMEOUT = 3000; // The timeout for the socket
/**
* Ping the server with the given hostname and port.
*
* @param hostname the hostname of the server
* @param ip the ip of the server, null if unresolved
* @param port the port of the server
* @param records the DNS records of the server
* @return the server that was pinged
*/
@Override
public JavaMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
log.info("Pinging {}:{}...", hostname, port);
try {
// Ping the server and retrieve both the status token, and the challenge status token
JavaServerStatusToken statusToken = retrieveStatusToken(hostname, port);
JavaServerChallengeStatusToken challengeStatusToken = null;
try {
challengeStatusToken = retrieveChallengeStatusToken(hostname, port);
} catch (Exception ex) {
// An exception will be raised if querying
// is disabled on the server. If the exception
// is not caused by querying being disabled, we
// want to log the error.
if (!(ex instanceof IOException)) {
log.error("Failed retrieving challenge status token for %s:%s:".formatted(hostname, port), ex);
}
}
// Return the server
return JavaMinecraftServer.create(hostname, ip, port, records, statusToken, challengeStatusToken);
} 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);
}
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
}
return null;
}
/**
* Ping a server and retrieve its response.
*
* @param hostname the hostname to ping
* @param port the port to ping
* @return the status token
* @throws IOException if an I/O error occurs
* @throws ResourceNotFoundException if the server didn't respond
*/
@NonNull
private JavaServerStatusToken retrieveStatusToken(@NonNull String hostname, int port) throws IOException, ResourceNotFoundException {
log.info("Opening TCP connection to {}:{}...", hostname, port);
long before = System.currentTimeMillis(); // Timestamp before pinging
// Open a socket connection to the server
try (Socket socket = new Socket()) {
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
long ping = System.currentTimeMillis() - before; // Calculate the ping
log.info("TCP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping);
// Begin packet transaction
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
// Begin handshaking with the server
new JavaHandshakingInSetProtocolPacket(hostname, port, JavaMinecraftVersion.getMinimumVersion().getProtocol())
.process(inputStream, outputStream);
// Send the status request to the server, and await back the response
JavaStatusInStartPacket packetStatusInStart = new JavaStatusInStartPacket();
packetStatusInStart.process(inputStream, outputStream);
String response = packetStatusInStart.getResponse();
if (response == null) { // No response
throw new ResourceNotFoundException("Server didn't respond to status request");
}
return AppConfig.GSON.fromJson(response, JavaServerStatusToken.class);
}
}
}
/**
* Ping a server and retrieve its challenge status token.
*
* @param hostname the hostname to ping
* @param port the port to ping
* @return the challenge token
* @throws IOException if an I/O error occurs
*/
@NonNull
private JavaServerChallengeStatusToken retrieveChallengeStatusToken(@NonNull String hostname, int port) throws IOException {
log.info("Opening UDP connection to {}:{}...", hostname, port);
long before = System.currentTimeMillis(); // Timestamp before pinging
// Open a socket connection to the server
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(500);
socket.connect(new InetSocketAddress(hostname, port));
long ping = System.currentTimeMillis() - before; // Calculate the ping
log.info("UDP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping);
// Begin handshaking with the server
new JavaQueryHandshakeRequestPacket().process(socket);
JavaQueryHandshakeResponsePacket handshakeResponse = new JavaQueryHandshakeResponsePacket();
handshakeResponse.process(socket);
// Send the full stats request to the server, and await back the response
new JavaQueryFullStatRequestPacket(handshakeResponse.getResponse()).process(socket);
JavaQueryFullStatResponsePacket fullStatResponse = new JavaQueryFullStatResponsePacket();
fullStatResponse.process(socket);
// Return the challenge token
return JavaServerChallengeStatusToken.create(fullStatResponse.getResponse());
}
}
}

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api;
package me.braydon.mc;
import lombok.NonNull;
import lombok.SneakyThrows;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import cc.restfulmc.api.model.dns.impl.ARecord;
import cc.restfulmc.api.model.dns.impl.SRVRecord;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import me.braydon.mc.model.dns.impl.ARecord;
import me.braydon.mc.model.dns.impl.SRVRecord;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.Type;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.Getter;
import lombok.experimental.UtilityClass;
@ -35,9 +35,8 @@ public final class EnvironmentUtils {
* Is the app running in a production environment?
*/
@Getter private static final boolean production;
static {
// Are we running on production?
String appEnv = System.getenv("APP_ENV");
production = appEnv != null && (appEnv.equals("production"));
static { // Are we running on production?
String env = System.getenv("APP_ENV");
production = env != null && (env.equals("production"));
}
}

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import net.jodah.expiringmap.ExpirationPolicy;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import lombok.SneakyThrows;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.Getter;
import lombok.NonNull;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
@ -36,7 +36,7 @@ import java.util.regex.Pattern;
@UtilityClass
public final class MiscUtils {
private static final Pattern USERNAME_REGEX = Pattern.compile("^[a-zA-Z0-9_]{2,16}$");
private static final List<String> WHITELISTED_USERNAMES = Arrays.asList("8", "g");
private static final List<String> WHITELISTED_NAMES = Arrays.asList("8", "g");
/**
* Check if the given username is a valid.
@ -45,6 +45,9 @@ public final class MiscUtils {
* @return whether the username is valid
*/
public static boolean isUsernameValid(@NonNull String username) {
return WHITELISTED_USERNAMES.contains(username.toLowerCase()) || USERNAME_REGEX.matcher(username).matches();
if (WHITELISTED_NAMES.contains(username.toLowerCase())) { // Name is whitelisted
return true;
}
return USERNAME_REGEX.matcher(username).matches();
}
}

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -39,20 +39,14 @@ import java.net.UnknownHostException;
*/
@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");
SESSION("https://sessionserver.mojang.com"),
API("https://api.mojang.com"),
TEXTURES("https://textures.minecraft.net"),
ASSETS("https://assets.mojang.com"),
LIBRARIES("https://libraries.minecraft.net");
private static final int STATUS_TIMEOUT = 7000;
/**
* The name of this server.
*/
@NonNull private final String name;
/**
* The endpoint of this service.
*/

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common;
package me.braydon.mc.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet;
package me.braydon.mc.common.packet;
import lombok.NonNull;
@ -35,12 +35,12 @@ import java.net.DatagramSocket;
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
*/
public abstract class UDPPacket {
public interface MinecraftBedrockPacket {
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
public abstract void process(@NonNull DatagramSocket socket) throws IOException;
void process(@NonNull DatagramSocket socket) throws IOException;
}

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet;
package me.braydon.mc.common.packet;
import lombok.NonNull;
@ -36,7 +36,7 @@ import java.io.IOException;
* @author Braydon
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
*/
public abstract class TCPPacket {
public abstract class MinecraftJavaPacket {
/**
* Process this packet.
*

View File

@ -21,10 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.bedrock;
package me.braydon.mc.common.packet.impl.bedrock;
import cc.restfulmc.api.common.packet.UDPPacket;
import lombok.NonNull;
import me.braydon.mc.common.packet.MinecraftBedrockPacket;
import java.io.IOException;
import java.net.DatagramPacket;
@ -40,7 +40,7 @@ import java.nio.ByteOrder;
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
*/
public final class BedrockUnconnectedPingPacket extends UDPPacket {
public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
private static final byte ID = 0x01; // The ID of the packet
private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };

View File

@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.bedrock;
package me.braydon.mc.common.packet.impl.bedrock;
import cc.restfulmc.api.common.packet.UDPPacket;
import cc.restfulmc.api.model.server.BedrockMinecraftServer;
import lombok.Getter;
import lombok.NonNull;
import me.braydon.mc.common.packet.MinecraftBedrockPacket;
import me.braydon.mc.model.server.BedrockMinecraftServer;
import java.io.IOException;
import java.net.DatagramPacket;
@ -37,13 +37,13 @@ import java.nio.charset.StandardCharsets;
/**
* This packet is sent by the server to the client in
* response to the {@link BedrockUnconnectedPingPacket}.
* response to the {@link BedrockPacketUnconnectedPing}.
*
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
*/
@Getter
public final class BedrockUnconnectedPongPacket extends UDPPacket {
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
private static final byte ID = 0x1C; // The ID of the packet
/**

View File

@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.tcp;
package me.braydon.mc.common.packet.impl.java;
import cc.restfulmc.api.common.packet.TCPPacket;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
import me.braydon.mc.common.packet.MinecraftJavaPacket;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@ -41,7 +41,7 @@ import java.io.IOException;
* @see <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
*/
@AllArgsConstructor @ToString
public final class JavaHandshakingInSetProtocolPacket extends TCPPacket {
public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket {
private static final byte ID = 0x00; // The ID of the packet
private static final int STATUS_HANDSHAKE = 1; // The status handshake ID

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.packet.impl.java.tcp;
package me.braydon.mc.common.packet.impl.java;
import cc.restfulmc.api.common.packet.TCPPacket;
import lombok.Getter;
import lombok.NonNull;
import me.braydon.mc.common.packet.MinecraftJavaPacket;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@ -40,7 +40,7 @@ import java.io.IOException;
* @see <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
*/
@Getter
public final class JavaStatusInStartPacket extends TCPPacket {
public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
private static final byte ID = 0x00; // The ID of the packet
/**

View File

@ -21,10 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.renderer;
package me.braydon.mc.common.renderer;
import cc.restfulmc.api.model.skin.ISkinPart;
import lombok.NonNull;
import me.braydon.mc.model.skin.ISkinPart;
import java.awt.*;
import java.awt.geom.AffineTransform;

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.renderer;
package me.braydon.mc.common.renderer;
import cc.restfulmc.api.common.ImageUtils;
import cc.restfulmc.api.model.skin.ISkinPart;
import cc.restfulmc.api.model.skin.Skin;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.braydon.mc.common.ImageUtils;
import me.braydon.mc.model.skin.ISkinPart;
import me.braydon.mc.model.skin.Skin;
import javax.imageio.ImageIO;
import java.awt.*;

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.renderer.impl;
package me.braydon.mc.common.renderer.impl;
import cc.restfulmc.api.common.ImageUtils;
import cc.restfulmc.api.common.renderer.SkinRenderer;
import cc.restfulmc.api.model.skin.ISkinPart;
import cc.restfulmc.api.model.skin.Skin;
import lombok.NonNull;
import me.braydon.mc.common.ImageUtils;
import me.braydon.mc.common.renderer.SkinRenderer;
import me.braydon.mc.model.skin.ISkinPart;
import me.braydon.mc.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;

View File

@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.renderer.impl;
package me.braydon.mc.common.renderer.impl;
import cc.restfulmc.api.common.renderer.IsometricSkinRenderer;
import cc.restfulmc.api.model.skin.ISkinPart;
import cc.restfulmc.api.model.skin.Skin;
import lombok.NonNull;
import me.braydon.mc.common.renderer.IsometricSkinRenderer;
import me.braydon.mc.model.skin.ISkinPart;
import me.braydon.mc.model.skin.Skin;
import java.awt.*;
import java.awt.geom.AffineTransform;

View File

@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.renderer.impl;
package me.braydon.mc.common.renderer.impl;
import cc.restfulmc.api.common.renderer.SkinRenderer;
import cc.restfulmc.api.model.skin.ISkinPart;
import cc.restfulmc.api.model.skin.Skin;
import lombok.NonNull;
import me.braydon.mc.common.renderer.SkinRenderer;
import me.braydon.mc.model.skin.ISkinPart;
import me.braydon.mc.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.web;
package me.braydon.mc.common.web;
import lombok.Getter;
import lombok.NonNull;

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.common.web;
package me.braydon.mc.common.web;
import cc.restfulmc.api.config.AppConfig;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import me.braydon.mc.config.AppConfig;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.config;
package me.braydon.mc.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.config;
package me.braydon.mc.config;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;

View File

@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.controller;
package me.braydon.mc.controller;
import cc.restfulmc.api.common.MojangServer;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.service.MojangService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.common.MojangServer;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@ -37,9 +37,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -70,17 +68,11 @@ public final class MojangController {
*/
@GetMapping("/status")
@ResponseBody
public ResponseEntity<Map<String, List<Map<String, Object>>>> getStatus() throws BadRequestException {
List<Map<String, Object>> servers = new ArrayList<>();
public ResponseEntity<Map<String, String>> getStatus() throws BadRequestException {
Map<String, String> response = new HashMap<>();
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangService.getMojangServerStatuses().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());
servers.add(serverStatus);
response.put(entry.getKey().getEndpoint(), entry.getValue().name());
}
return ResponseEntity.ok(Map.of("servers", servers));
return ResponseEntity.ok(response);
}
}

View File

@ -21,18 +21,18 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.controller;
package me.braydon.mc.controller;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.exception.impl.MojangRateLimitException;
import cc.restfulmc.api.exception.impl.ResourceNotFoundException;
import cc.restfulmc.api.model.Player;
import cc.restfulmc.api.model.cache.CachedPlayer;
import cc.restfulmc.api.service.MojangService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.MojangRateLimitException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

View File

@ -21,17 +21,17 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.controller;
package me.braydon.mc.controller;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.exception.impl.ResourceNotFoundException;
import cc.restfulmc.api.model.MinecraftServer;
import cc.restfulmc.api.model.cache.CachedMinecraftServer;
import cc.restfulmc.api.service.MojangService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

View File

@ -21,11 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.exception;
package me.braydon.mc.exception;
import cc.restfulmc.api.model.response.ErrorResponse;
import io.sentry.Sentry;
import lombok.NonNull;
import me.braydon.mc.model.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -67,7 +66,6 @@ public final class ExceptionControllerAdvice {
}
if (status == null) { // Fallback to 500
status = HttpStatus.INTERNAL_SERVER_ERROR;
Sentry.captureException(ex); // Capture with Sentry
}
return new ResponseEntity<>(new ErrorResponse(status, message), status);
}

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.exception.impl;
package me.braydon.mc.exception.impl;
import lombok.experimental.StandardException;
import org.springframework.http.HttpStatus;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.exception.impl;
package me.braydon.mc.exception.impl;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.exception.impl;
package me.braydon.mc.exception.impl;
import lombok.experimental.StandardException;
import org.springframework.http.HttpStatus;

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.log;
package me.braydon.mc.log;
import cc.restfulmc.api.common.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import me.braydon.mc.common.IPUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model;
package me.braydon.mc.model;
import com.google.gson.JsonObject;
import lombok.*;

View File

@ -21,20 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model;
package me.braydon.mc.model;
import cc.restfulmc.api.common.ColorUtils;
import cc.restfulmc.api.model.dns.DNSRecord;
import cc.restfulmc.api.model.token.JavaServerStatusToken;
import cc.restfulmc.api.service.pinger.MinecraftServerPinger;
import cc.restfulmc.api.service.pinger.impl.BedrockMinecraftServerPinger;
import cc.restfulmc.api.service.pinger.impl.JavaMinecraftServerPinger;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.record.City;
import com.maxmind.geoip2.record.Continent;
import com.maxmind.geoip2.record.Country;
import com.maxmind.geoip2.record.Location;
import lombok.*;
import me.braydon.mc.common.ColorUtils;
import me.braydon.mc.model.dns.DNSRecord;
import me.braydon.mc.model.token.JavaServerStatusToken;
import me.braydon.mc.service.pinger.MinecraftServerPinger;
import me.braydon.mc.service.pinger.impl.BedrockMinecraftServerPinger;
import me.braydon.mc.service.pinger.impl.JavaMinecraftServerPinger;
import java.util.ArrayList;
import java.util.Arrays;
@ -46,7 +41,7 @@ import java.util.UUID;
*
* @author Braydon
*/
@AllArgsConstructor @Setter @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
@AllArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
public class MinecraftServer {
/**
* The hostname of this server.
@ -64,14 +59,9 @@ public class MinecraftServer {
@EqualsAndHashCode.Include private final int port;
/**
* The DNS records resolved for this server, null if none.
* The DNS records resolved for this server.
*/
private final DNSRecord[] records;
/**
* The Geo location of this server, null if unknown.
*/
private GeoLocation geo;
@NonNull private final DNSRecord[] records;
/**
* The player counts of this server.
@ -83,74 +73,6 @@ public class MinecraftServer {
*/
@NonNull private final MOTD motd;
/**
* The Geo location of a server.
*/
@AllArgsConstructor @Getter @ToString
public static class GeoLocation {
/**
* The continent of this server.
*/
@NonNull private final LocationData continent;
/**
* The country of this server.
*/
@NonNull private final LocationData country;
/**
* The city of this server, null if unknown.
*/
private final String city;
/**
* The latitude of this server.
*/
private final double latitude;
/**
* The longitude of this server.
*/
private final double longitude;
/**
* Create new geo location data
* from the given city response.
*
* @param geo the geo city response
* @return the geo location
*/
@NonNull
public static GeoLocation create(@NonNull CityResponse geo) {
Continent continent = geo.getContinent();
Country country = geo.getCountry();
City city = geo.getCity();
Location location = geo.getLocation();
return new GeoLocation(
new LocationData(continent.getCode(), continent.getName()),
new LocationData(country.getIsoCode(), country.getName()),
city == null ? null : city.getName(),
location.getLatitude(), location.getLongitude()
);
}
/**
* Data for a location.
*/
@AllArgsConstructor @Getter @ToString
public static class LocationData {
/**
* The location code.
*/
@NonNull private final String code;
/**
* The location name.
*/
@NonNull private final String name;
}
}
/**
* Player count data for a server.
*/

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model;
package me.braydon.mc.model;
import cc.restfulmc.api.model.skin.Skin;
import cc.restfulmc.api.model.token.MojangProfileToken;
import lombok.*;
import me.braydon.mc.model.skin.Skin;
import me.braydon.mc.model.token.MojangProfileToken;
import java.util.UUID;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model;
package me.braydon.mc.model;
/**
* Profile actions that can

View File

@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.cache;
package me.braydon.mc.model.cache;
import cc.restfulmc.api.model.MinecraftServer;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.*;
import me.braydon.mc.model.MinecraftServer;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

View File

@ -21,18 +21,18 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.cache;
package me.braydon.mc.model.cache;
import cc.restfulmc.api.model.Cape;
import cc.restfulmc.api.model.Player;
import cc.restfulmc.api.model.ProfileAction;
import cc.restfulmc.api.model.skin.Skin;
import cc.restfulmc.api.model.token.MojangProfileToken;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import me.braydon.mc.model.Cape;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.ProfileAction;
import me.braydon.mc.model.skin.Skin;
import me.braydon.mc.model.token.MojangProfileToken;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.cache;
package me.braydon.mc.model.cache;
import lombok.*;
import org.springframework.data.annotation.Id;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.cache;
package me.braydon.mc.model.cache;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.dns;
package me.braydon.mc.model.dns;
import lombok.*;

View File

@ -21,10 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.dns.impl;
package me.braydon.mc.model.dns.impl;
import cc.restfulmc.api.model.dns.DNSRecord;
import lombok.*;
import me.braydon.mc.model.dns.DNSRecord;
import java.net.InetAddress;

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.dns.impl;
package me.braydon.mc.model.dns.impl;
import cc.restfulmc.api.model.dns.DNSRecord;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import me.braydon.mc.model.dns.DNSRecord;
import java.net.InetSocketAddress;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.response;
package me.braydon.mc.model.response;
import lombok.Getter;
import lombok.NonNull;

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.server;
package me.braydon.mc.model.server;
import cc.restfulmc.api.model.MinecraftServer;
import cc.restfulmc.api.model.dns.DNSRecord;
import lombok.*;
import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.dns.DNSRecord;
/**
* A Bedrock edition {@link MinecraftServer}.
@ -54,10 +54,10 @@ public final class BedrockMinecraftServer extends MinecraftServer {
*/
@NonNull private final GameMode gamemode;
private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, GeoLocation geo,
DNSRecord[] records, @NonNull Edition edition, @NonNull Version version,
@NonNull Players players, @NonNull MOTD motd, @NonNull GameMode gamemode) {
super(hostname, ip, port, records, geo, players, motd);
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, players, motd);
this.id = id;
this.edition = edition;
this.version = version;
@ -70,19 +70,20 @@ public final class BedrockMinecraftServer extends MinecraftServer {
* @param hostname the hostname of the server
* @param ip the IP address of the server
* @param port the port of the server
* @param records the DNS records of the server, if any
* @param records the DNS records of the server
* @param token the status token
* @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,
@NonNull DNSRecord[] records, @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]);
GameMode gameMode = new GameMode(split[8], split.length > 9 ? Integer.parseInt(split[9]) : -1);
return new BedrockMinecraftServer(split[6], hostname, ip, port, null, records, edition, version, players, motd, gameMode);
return new BedrockMinecraftServer(split[6], hostname, ip, port, records, edition, version, players, motd, gameMode);
}
/**

View File

@ -21,24 +21,19 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.server;
package me.braydon.mc.model.server;
import cc.restfulmc.api.common.JavaMinecraftVersion;
import cc.restfulmc.api.config.AppConfig;
import cc.restfulmc.api.model.MinecraftServer;
import cc.restfulmc.api.model.dns.DNSRecord;
import cc.restfulmc.api.model.token.JavaServerChallengeStatusToken;
import cc.restfulmc.api.model.token.JavaServerStatusToken;
import cc.restfulmc.api.service.MojangService;
import com.google.gson.annotations.SerializedName;
import lombok.*;
import me.braydon.mc.common.JavaMinecraftVersion;
import me.braydon.mc.config.AppConfig;
import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.dns.DNSRecord;
import me.braydon.mc.model.token.JavaServerStatusToken;
import me.braydon.mc.service.MojangService;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A Java edition {@link MinecraftServer}.
*
@ -56,17 +51,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
*/
private final Favicon favicon;
/**
* The software of this server, present if query is on.
*/
private final String software;
/**
* The plugins on this server, present if
* query is on and plugins are present.
*/
private final Plugin[] plugins;
/**
* The Forge mod information for this server, null if none.
* <p>
@ -83,16 +67,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
*/
private final ForgeData forgeData;
/**
* The main world of this server, present if query is on.
*/
private final String world;
/**
* Does this server support querying?
*/
private final boolean queryEnabled;
/**
* Does this server preview chat?
*
@ -123,20 +97,14 @@ public final class JavaMinecraftServer extends MinecraftServer {
*/
private boolean mojangBanned;
private JavaMinecraftServer(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation geo,
@NonNull Version version, @NonNull Players players, @NonNull MOTD motd, Favicon favicon,
String software, Plugin[] plugins, ModInfo modInfo, ForgeData forgeData, String world,
boolean queryEnabled, boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports,
boolean mojangBanned) {
super(hostname, ip, port, records, geo, players, motd);
private JavaMinecraftServer(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records, @NonNull Version version,
@NonNull Players players, @NonNull MOTD motd, Favicon favicon, ModInfo modInfo, ForgeData forgeData,
boolean previewsChat, boolean enforcesSecureChat, boolean preventsChatReports, boolean mojangBanned) {
super(hostname, ip, port, records, players, motd);
this.version = version;
this.favicon = favicon;
this.software = software;
this.plugins = plugins;
this.modInfo = modInfo;
this.forgeData = forgeData;
this.world = world;
this.queryEnabled = queryEnabled;
this.previewsChat = previewsChat;
this.enforcesSecureChat = enforcesSecureChat;
this.preventsChatReports = preventsChatReports;
@ -149,35 +117,20 @@ public final class JavaMinecraftServer extends MinecraftServer {
* @param hostname the hostname of the server
* @param ip the IP address of the server
* @param port the port of the server
* @param records the DNS records of the server, if any
* @param statusToken the status token
* @param challengeStatusToken the challenge status token, null if none
* @param records the DNS records of the server
* @param token the status token
* @return the Java Minecraft server
*/
@NonNull
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records,
@NonNull JavaServerStatusToken statusToken, JavaServerChallengeStatusToken challengeStatusToken) {
String motdString = statusToken.getDescription() instanceof String ? (String) statusToken.getDescription() : null;
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port,
@NonNull DNSRecord[] records, @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(AppConfig.GSON.toJson(statusToken.getDescription()))).toLegacyText();
motdString = new TextComponent(ComponentSerializer.parse(AppConfig.GSON.toJson(token.getDescription()))).toLegacyText();
}
String software = challengeStatusToken == null ? null : challengeStatusToken.getSoftware(); // The server software
// Get the plugins from the challenge token
Plugin[] plugins = null;
if (challengeStatusToken != null) {
List<Plugin> list = new ArrayList<>();
for (Map.Entry<String, String> entry : challengeStatusToken.getPlugins().entrySet()) {
list.add(new Plugin(entry.getKey(), entry.getValue()));
}
plugins = list.toArray(new Plugin[0]);
}
String world = challengeStatusToken == null ? null : challengeStatusToken.getMap(); // The main server world
return new JavaMinecraftServer(hostname, ip, port, records, null, statusToken.getVersion().detailedCopy(), Players.create(statusToken.getPlayers()),
MOTD.create(motdString), Favicon.create(statusToken.getFavicon(), hostname), software, plugins, statusToken.getModInfo(),
statusToken.getForgeData(), world, challengeStatusToken != null, statusToken.isPreviewsChat(),
statusToken.isEnforcesSecureChat(), statusToken.isPreventsChatReports(), false
return new JavaMinecraftServer(hostname, ip, port, records, token.getVersion().detailedCopy(), Players.create(token.getPlayers()),
MOTD.create(motdString), Favicon.create(token.getFavicon(), hostname), token.getModInfo(), token.getForgeData(),
token.isPreviewsChat(), token.isEnforcesSecureChat(), token.isPreventsChatReports(), false
);
}
@ -227,9 +180,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
}
}
JavaMinecraftVersion minecraftVersion = JavaMinecraftVersion.byProtocol(protocol);
if (minecraftVersion == JavaMinecraftVersion.UNKNOWN) {
minecraftVersion = null;
}
return new Version(name, platform, protocol, supportedVersions, minecraftVersion == null ? null : minecraftVersion.getName());
}
}
@ -267,22 +217,6 @@ public final class JavaMinecraftServer extends MinecraftServer {
}
}
/**
* A plugin for a server.
*/
@AllArgsConstructor @Getter @ToString
public static class Plugin {
/**
* The name of this plugin.
*/
@NonNull private final String name;
/**
* The version of this plugin.
*/
@NonNull private final String version;
}
/**
* Forge mod information for a server.
* <p>

View File

@ -21,13 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.skin;
package me.braydon.mc.model.skin;
import cc.restfulmc.api.common.renderer.SkinRenderer;
import cc.restfulmc.api.common.renderer.impl.BodySkinPartRenderer;
import cc.restfulmc.api.common.renderer.impl.IsometricHeadSkinPartRenderer;
import cc.restfulmc.api.common.renderer.impl.VanillaSkinPartRenderer;
import lombok.*;
import me.braydon.mc.common.renderer.SkinRenderer;
import me.braydon.mc.common.renderer.impl.BodySkinPartRenderer;
import me.braydon.mc.common.renderer.impl.IsometricHeadSkinPartRenderer;
import me.braydon.mc.common.renderer.impl.VanillaSkinPartRenderer;
import java.awt.image.BufferedImage;

View File

@ -21,15 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.skin;
package me.braydon.mc.model.skin;
import cc.restfulmc.api.common.ImageUtils;
import cc.restfulmc.api.config.AppConfig;
import cc.restfulmc.api.model.Player;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject;
import lombok.*;
import me.braydon.mc.common.ImageUtils;
import me.braydon.mc.config.AppConfig;
import me.braydon.mc.model.Player;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

View File

@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.token;
package me.braydon.mc.model.token;
import cc.restfulmc.api.model.server.JavaMinecraftServer;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import me.braydon.mc.model.server.JavaMinecraftServer;
import java.util.UUID;

View File

@ -21,15 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.token;
package me.braydon.mc.model.token;
import cc.restfulmc.api.config.AppConfig;
import cc.restfulmc.api.model.Cape;
import cc.restfulmc.api.model.ProfileAction;
import cc.restfulmc.api.model.skin.Skin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.gson.JsonObject;
import lombok.*;
import me.braydon.mc.config.AppConfig;
import me.braydon.mc.model.Cape;
import me.braydon.mc.model.ProfileAction;
import me.braydon.mc.model.skin.Skin;
import java.util.Base64;

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.model.token;
package me.braydon.mc.model.token;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -21,9 +21,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.repository;
package me.braydon.mc.repository;
import cc.restfulmc.api.model.cache.CachedMinecraftServer;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import org.springframework.data.repository.CrudRepository;
/**

View File

@ -21,9 +21,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.repository;
package me.braydon.mc.repository;
import cc.restfulmc.api.model.cache.CachedPlayer;
import me.braydon.mc.model.cache.CachedPlayer;
import org.springframework.data.repository.CrudRepository;
/**

View File

@ -21,9 +21,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.repository;
package me.braydon.mc.repository;
import cc.restfulmc.api.model.cache.CachedPlayerName;
import me.braydon.mc.model.cache.CachedPlayerName;
import org.springframework.data.repository.CrudRepository;
/**

View File

@ -21,10 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.repository;
package me.braydon.mc.repository;
import cc.restfulmc.api.model.cache.CachedSkinPartTexture;
import cc.restfulmc.api.model.skin.ISkinPart;
import me.braydon.mc.model.cache.CachedSkinPartTexture;
import me.braydon.mc.model.skin.ISkinPart;
import org.springframework.data.repository.CrudRepository;
/**

View File

@ -21,44 +21,42 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.service;
package me.braydon.mc.service;
import cc.restfulmc.api.common.*;
import cc.restfulmc.api.common.web.JsonWebException;
import cc.restfulmc.api.common.web.JsonWebRequest;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.exception.impl.MojangRateLimitException;
import cc.restfulmc.api.exception.impl.ResourceNotFoundException;
import cc.restfulmc.api.model.MinecraftServer;
import cc.restfulmc.api.model.Player;
import cc.restfulmc.api.model.ProfileAction;
import cc.restfulmc.api.model.cache.CachedMinecraftServer;
import cc.restfulmc.api.model.cache.CachedPlayer;
import cc.restfulmc.api.model.cache.CachedPlayerName;
import cc.restfulmc.api.model.cache.CachedSkinPartTexture;
import cc.restfulmc.api.model.dns.DNSRecord;
import cc.restfulmc.api.model.dns.impl.ARecord;
import cc.restfulmc.api.model.dns.impl.SRVRecord;
import cc.restfulmc.api.model.server.JavaMinecraftServer;
import cc.restfulmc.api.model.skin.ISkinPart;
import cc.restfulmc.api.model.skin.Skin;
import cc.restfulmc.api.model.token.MojangProfileToken;
import cc.restfulmc.api.model.token.MojangUsernameToUUIDToken;
import cc.restfulmc.api.repository.MinecraftServerCacheRepository;
import cc.restfulmc.api.repository.PlayerCacheRepository;
import cc.restfulmc.api.repository.PlayerNameCacheRepository;
import cc.restfulmc.api.repository.SkinPartTextureCacheRepository;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.maxmind.geoip2.model.CityResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.common.*;
import me.braydon.mc.common.web.JsonWebException;
import me.braydon.mc.common.web.JsonWebRequest;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.MojangRateLimitException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.ProfileAction;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.model.cache.CachedPlayerName;
import me.braydon.mc.model.cache.CachedSkinPartTexture;
import me.braydon.mc.model.dns.DNSRecord;
import me.braydon.mc.model.dns.impl.ARecord;
import me.braydon.mc.model.dns.impl.SRVRecord;
import me.braydon.mc.model.server.JavaMinecraftServer;
import me.braydon.mc.model.skin.ISkinPart;
import me.braydon.mc.model.skin.Skin;
import me.braydon.mc.model.token.MojangProfileToken;
import me.braydon.mc.model.token.MojangUsernameToUUIDToken;
import me.braydon.mc.repository.MinecraftServerCacheRepository;
import me.braydon.mc.repository.PlayerCacheRepository;
import me.braydon.mc.repository.PlayerNameCacheRepository;
import me.braydon.mc.repository.SkinPartTextureCacheRepository;
import net.jodah.expiringmap.ExpirationPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
@ -66,7 +64,6 @@ import org.springframework.stereotype.Service;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@ -93,11 +90,6 @@ public final class MojangService {
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private static final Joiner DOT_JOINER = Joiner.on('.');
/**
* The MaxMind service to use for Geo lookups.
*/
@NonNull private final MaxMindService maxMindService;
/**
* The cache repository for {@link Player}'s by their username.
*/
@ -142,9 +134,8 @@ public final class MojangService {
private final ExpiringSet<String> blockedServersCache = new ExpiringSet<>(ExpirationPolicy.CREATED, 10L, TimeUnit.MINUTES);
@Autowired
public MojangService(@NonNull MaxMindService maxMindService, @NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache,
public MojangService(@NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache,
@NonNull SkinPartTextureCacheRepository skinPartTextureCache, @NonNull MinecraftServerCacheRepository minecraftServerCache) {
this.maxMindService = maxMindService;
this.playerNameCache = playerNameCache;
this.playerCache = playerCache;
this.skinPartTextureCache = skinPartTextureCache;
@ -163,13 +154,13 @@ public final class MojangService {
}, 0L, 60L * 3L * 1000L);
// Schedule a task to fetch blocked
// servers from Mojang every hour.
// servers from Mojang every 15 minutes.
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
fetchBlockedServers();
}
}, 0L, 60L * 60L * 1000L);
}, 0L, 60L * 15L * 1000L);
}
/**
@ -356,13 +347,10 @@ public final class MojangService {
} catch (BadRequestException | ResourceNotFoundException ignored) {
// Safely ignore these, we will use the default server icon
}
try {
assert icon != null;
return Base64.getDecoder().decode(icon); // Return the decoded favicon
} catch (Exception ex) { // Use the default server icon
log.error("Failed getting server favicon for %s:".formatted(hostname), ex);
return Base64.getDecoder().decode(DEFAULT_SERVER_ICON);
if (icon == null) { // Use the default server icon
icon = DEFAULT_SERVER_ICON;
}
return Base64.getDecoder().decode(icon); // Return the decoded favicon
}
/**
@ -441,10 +429,9 @@ public final class MojangService {
throw new BadRequestException("Invalid port defined");
}
}
String cacheKey = "%s-%s".formatted(platform.name(), lookupHostname.replace(":", "-"));
// Check the cache for the server
Optional<CachedMinecraftServer> cached = minecraftServerCache.findById(cacheKey);
Optional<CachedMinecraftServer> cached = minecraftServerCache.findById("%s-%s".formatted(platform.name(), lookupHostname));
if (cached.isPresent()) { // Respond with the cache if present
log.info("Found server in cache: {}", hostname);
return cached.get();
@ -466,28 +453,13 @@ public final class MojangService {
log.info("Resolved hostname: {} -> {}", hostname, ip);
}
// Attempt to perform a Geo lookup on the server
CityResponse geo = null; // The server's Geo location
try {
log.info("Looking up Geo location data for {}...", ip);
InetAddress address = InetAddress.getByName(ip == null ? hostname : ip);
geo = maxMindService.lookupCity(address);
} catch (Exception ex) {
log.error("Failed looking up Geo location data for %s:".formatted(ip), ex);
}
// Build our server model, cache it, and then return it
MinecraftServer response = platform.getPinger().ping(hostname, ip, port, records.toArray(new DNSRecord[0])); // Ping the server and await a response
if (response == null) { // No response from ping
throw new ResourceNotFoundException("Server didn't respond to ping");
}
if (geo != null) { // Update Geo location data in the server if present
response.setGeo(MinecraftServer.GeoLocation.create(geo));
}
CachedMinecraftServer minecraftServer = new CachedMinecraftServer(
cacheKey, response, System.currentTimeMillis()
platform.name() + "-" + lookupHostname, response, System.currentTimeMillis()
);
// Get the blocked status of the Java server
@ -593,14 +565,4 @@ public final class MojangService {
}
return blocked;
}
/**
* Cleanup when the app is destroyed.
*/
@PreDestroy
public void cleanup() {
mojangServerStatuses.clear();
bannedServerHashes.clear();
blockedServersCache.clear();
}
}

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.service.pinger;
package me.braydon.mc.service.pinger;
import cc.restfulmc.api.model.MinecraftServer;
import cc.restfulmc.api.model.dns.DNSRecord;
import lombok.NonNull;
import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.dns.DNSRecord;
/**
* A {@link MinecraftServerPinger} is

View File

@ -21,17 +21,17 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.service.pinger.impl;
package me.braydon.mc.service.pinger.impl;
import cc.restfulmc.api.common.packet.impl.bedrock.BedrockUnconnectedPingPacket;
import cc.restfulmc.api.common.packet.impl.bedrock.BedrockUnconnectedPongPacket;
import cc.restfulmc.api.exception.impl.BadRequestException;
import cc.restfulmc.api.exception.impl.ResourceNotFoundException;
import cc.restfulmc.api.model.dns.DNSRecord;
import cc.restfulmc.api.model.server.BedrockMinecraftServer;
import cc.restfulmc.api.service.pinger.MinecraftServerPinger;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPing;
import me.braydon.mc.common.packet.impl.bedrock.BedrockPacketUnconnectedPong;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.dns.DNSRecord;
import me.braydon.mc.model.server.BedrockMinecraftServer;
import me.braydon.mc.service.pinger.MinecraftServerPinger;
import java.io.IOException;
import java.net.DatagramSocket;
@ -60,7 +60,7 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
*/
@Override
public BedrockMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
log.info("Opening UDP connection to {}:{}...", hostname, port);
log.info("Pinging {}:{}...", hostname, port);
long before = System.currentTimeMillis(); // Timestamp before pinging
// Open a socket connection to the server
@ -69,13 +69,13 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
socket.connect(new InetSocketAddress(hostname, port));
long ping = System.currentTimeMillis() - before; // Calculate the ping
log.info("UDP Connection to {}:{} opened. Ping: {}ms", hostname, port, ping);
log.info("Pinged {}:{} in {}ms", hostname, port, ping);
// Send the unconnected ping packet
new BedrockUnconnectedPingPacket().process(socket);
new BedrockPacketUnconnectedPing().process(socket);
// Handle the received unconnected pong packet
BedrockUnconnectedPongPacket unconnectedPong = new BedrockUnconnectedPongPacket();
BedrockPacketUnconnectedPong unconnectedPong = new BedrockPacketUnconnectedPong();
unconnectedPong.process(socket);
String response = unconnectedPong.getResponse();
if (response == null) { // No pong response

View File

@ -0,0 +1,103 @@
/*
* MIT License
*
* Copyright (c) 2024 Braydon (Rainnny).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.braydon.mc.service.pinger.impl;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.common.JavaMinecraftVersion;
import me.braydon.mc.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
import me.braydon.mc.common.packet.impl.java.JavaPacketStatusInStart;
import me.braydon.mc.config.AppConfig;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.dns.DNSRecord;
import me.braydon.mc.model.server.JavaMinecraftServer;
import me.braydon.mc.model.token.JavaServerStatusToken;
import me.braydon.mc.service.pinger.MinecraftServerPinger;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.*;
/**
* The {@link MinecraftServerPinger} for pinging
* {@link JavaMinecraftServer}'s over TCP.
*
* @author Braydon
*/
@Log4j2(topic = "Java MC Server Pinger")
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
private static final int TIMEOUT = 3000; // The timeout for the socket
/**
* Ping the server with the given hostname and port.
*
* @param hostname the hostname of the server
* @param ip the ip of the server, null if unresolved
* @param port the port of the server
* @param records the DNS records of the server
* @return the server that was pinged
*/
@Override
public JavaMinecraftServer ping(@NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records) {
log.info("Pinging {}:{}...", hostname, port);
long before = System.currentTimeMillis(); // Timestamp before pinging
// Open a socket connection to the server
try (Socket socket = new Socket()) {
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
long ping = System.currentTimeMillis() - before; // Calculate the ping
log.info("Pinged {}:{} in {}ms", hostname, port, ping);
// Open data streams to begin packet transaction
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
// Begin handshaking with the server
new JavaPacketHandshakingInSetProtocol(hostname, port, JavaMinecraftVersion.getMinimumVersion().getProtocol())
.process(inputStream, outputStream);
// Send the status request to the server, and await back the response
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream);
String response = packetStatusInStart.getResponse();
if (response == null) { // No response
throw new ResourceNotFoundException("Server didn't respond to status request");
}
JavaServerStatusToken token = AppConfig.GSON.fromJson(response, JavaServerStatusToken.class);
return JavaMinecraftServer.create(hostname, ip, port, records, token); // Return the server
}
} 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);
}
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
}
return null;
}
}

View File

@ -11,16 +11,6 @@ logging:
file:
path: "./logs"
# Sentry Configuration
sentry:
dsn: https://87487c1562d043f79c09e77e4bc359b8@sentry.rainnny.club/2
tracesSampleRate: 1.0
# MaxMind Configuration
# Used for IP Geo location
maxmind:
license: "CHANGE_ME"
# Spring Configuration
spring:
data:

View File

@ -4,11 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A simple, yet useful RESTful API for Minecraft utilizing Springboot.">
<meta name="theme-color" content="#3C8627">
<meta name="theme-color" content="#3A34EB">
<title>RESTfulMC</title>
</head>
<body>
<h1>RESTfulMC API</h1>
<h1>RESTfulMC</h1>
<p>A simple, yet useful RESTful API for Minecraft utilizing Springboot.</p>
<p>View the source <a href="https://git.rainnny.club/Rainnny/RESTfulMC">here</a>!</p>
</body>

View File

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.test.config;
package me.braydon.mc.test.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.test.controller;
package me.braydon.mc.test.controller;
import cc.restfulmc.api.controller.MojangController;
import cc.restfulmc.api.test.config.TestRedisConfig;
import lombok.NonNull;
import me.braydon.mc.controller.MojangController;
import me.braydon.mc.test.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.test.controller;
package me.braydon.mc.test.controller;
import cc.restfulmc.api.controller.PlayerController;
import cc.restfulmc.api.test.config.TestRedisConfig;
import lombok.NonNull;
import me.braydon.mc.controller.PlayerController;
import me.braydon.mc.test.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

View File

@ -21,11 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cc.restfulmc.api.test.controller;
package me.braydon.mc.test.controller;
import cc.restfulmc.api.controller.ServerController;
import cc.restfulmc.api.test.config.TestRedisConfig;
import lombok.NonNull;
import me.braydon.mc.controller.ServerController;
import me.braydon.mc.test.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

29
DemoPlugin/.gitignore vendored
View File

@ -1,29 +0,0 @@
*.class
*.log
*.ctxt
.mtj.tmp/
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
replay_pid*
.idea
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
build/
jars/
target/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
git.properties
pom.xml.versionsBackup

View File

@ -1,5 +0,0 @@
# RESTfulMC Demo Plugin
This plugin provides an example of what can be returned from the API.
## Demo Server
Ping `demo.restfulmc.cc` on Java or Bedrock, or better yet, view it [here](https://restfulmc.cc/server/java/demo.restfulmc.cc)

View File

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--Project Details-->
<groupId>me.braydon</groupId>
<artifactId>DemoPlugin</artifactId>
<version>1.0.0</version>
<!-- Properties -->
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Used for compiling the source code with the proper Java version -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<!-- Enable incremental builds, this is reversed due to -->
<!-- a bug as seen in https://issues.apache.org/jira/browse/MCOMPILER-209 -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<!-- Handles shading of dependencies in the final output jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<!-- Filter the resources dir for placeholders -->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<!-- Repositories -->
<repositories>
<!-- Used by Velocity -->
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<!-- Dependencies -->
<dependencies>
<!-- Server Jars -->
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.3.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,34 +0,0 @@
package cc.restfulmc.demo;
import cc.restfulmc.demo.listener.ServerPingListener;
import com.google.inject.Inject;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import net.kyori.adventure.text.Component;
/**
* @author Braydon
*/
@Plugin(id = "demoplugin", name = "DemoPlugin", version = "1.0.0")
public final class DemoPlugin {
private final ProxyServer server;
@Inject
public DemoPlugin(ProxyServer server) {
this.server = server;
}
@Subscribe
public void onProxyInitialize(ProxyInitializeEvent event) {
server.getEventManager().register(this, new ServerPingListener());
}
@Subscribe(order = PostOrder.FIRST)
public void onLogin(PreLoginEvent event) {
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Component.text("§cYou can't join a demo server :(")));
}
}

View File

@ -1,61 +0,0 @@
package cc.restfulmc.demo.listener;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.ModInfo;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author Braydon
*/
public final class ServerPingListener {
private static final String[] MESSAGES = new String[] {
"wow omg so cool!",
"Hello World!",
"Rainnny was here",
"Star on GitHub!",
"restfulmc.cc",
"discord.restfulmc.cc"
};
private static final String[] PLAYERS = new String[] {
"Rainnny", "Notch", "jeb_", "hypixel", "Dinnerbone", "C418", "g", "hey"
};
@Subscribe
public void onProxyPing(ProxyPingEvent event) {
ServerPing ping = event.getPing(); // Get the ping response
ThreadLocalRandom random = ThreadLocalRandom.current();
// Update the version
ServerPing.Version version = new ServerPing.Version(ping.getVersion().getProtocol(), "RESTfulMC Demo");
// Update the player count
List<ServerPing.SamplePlayer> playerSamples = new ArrayList<>();
for (int i = 0; i < 3; i++) {
playerSamples.add(new ServerPing.SamplePlayer(PLAYERS[random.nextInt(PLAYERS.length)], UUID.randomUUID()));
}
ServerPing.Players players = new ServerPing.Players(random.nextInt(300, 25000), 30000, playerSamples);
TextComponent motd = Component.text(String.join("\n",
"§f §2§lRESTfulMC §7Demo Server",
"§7 " + MESSAGES[random.nextInt(MESSAGES.length)]
));
// Update the mod info
ModInfo modInfo = new ModInfo(ModInfo.DEFAULT.getType(), Arrays.asList(
new ModInfo.Mod("bob", "1.0"),
new ModInfo.Mod("ross", "1.0")
));
// Set the ping response
event.setPing(new ServerPing(version, players, motd, ping.getFavicon().orElse(null), modInfo));
}
}

28
DiscordBot/.gitignore vendored
View File

@ -1,28 +0,0 @@
*.class
*.log
*.ctxt
.mtj.tmp/
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
replay_pid*
.idea
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
build/
target/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
git.properties
pom.xml.versionsBackup

View File

@ -1,13 +0,0 @@
FROM maven:3.8.5-openjdk-17-slim
# Set the working directory
WORKDIR /home/container
# Copy project files
COPY . .
# Build the app
RUN mvn clean package -q -T4C
# Start the app
CMD ["java", "-jar", "target/DiscordBot.jar"]

View File

@ -1,2 +0,0 @@
# RESTfulMC Discord Bot
This Discord bot allows you to interact with the API directly from Discord. Want to try it out? Join the [RESTfulMC Discord](https://discord.restfulmc.cc)

View File

@ -1,122 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--Project Details-->
<groupId>cc.restfulmc</groupId>
<artifactId>DiscordBot</artifactId>
<version>1.0.0</version>
<!-- Properties -->
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>2.1.0-alpha1</slf4j.version>
</properties>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Used for compiling the source code with the proper Java version -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<!-- Enable incremental builds, this is reversed due to -->
<!-- a bug as seen in https://issues.apache.org/jira/browse/MCOMPILER-209 -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<!-- Handles shading of dependencies in the final output jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Specify the apps main class -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>cc.restfulmc.bot.DiscordBot</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<!-- Repositories -->
<repositories>
<repository>
<id>rainnny-repo-public</id>
<url>https://maven.rainnny.club/public</url>
</repository>
</repositories>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<!-- Libraries -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-beta.23</version>
<exclusions>
<exclusion>
<groupId>club.minnced</groupId>
<artifactId>opus-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cc.restfulmc</groupId>
<artifactId>Java-SDK</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,62 +0,0 @@
package cc.restfulmc.bot;
import cc.restfulmc.bot.command.CommandManager;
import cc.restfulmc.sdk.client.ClientConfig;
import cc.restfulmc.sdk.client.RESTfulMCClient;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.SelfUser;
/**
* @author Braydon
*/
@Slf4j(topic = "RESTfulMC Bot") @Getter
public final class DiscordBot {
/**
* The JDA bot instance.
*/
private JDA jda;
@SneakyThrows
public DiscordBot() {
String token = System.getenv("BOT_TOKEN");
if (token == null) { // Missing BOT_TOKEN
throw new NullPointerException("Missing BOT_TOKEN environment variable");
}
jda = JDABuilder.createLight(token)
.setActivity(Activity.watching("Minecraft servers"))
.build();
jda.awaitReady(); // Wait for JDA to become ready
// Setup the API SDK
RESTfulMCClient apiClient = new RESTfulMCClient(ClientConfig.defaultConfig());
// Commands
new CommandManager(this, apiClient);
SelfUser self = jda.getSelfUser();
log.info("Logged in as bot {} ({})", self.getAsTag(), self.getId());
// Add a cleanup hook to cleanup the bot when the JVM shuts down
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
}
/**
* Cleanup the bot.
*/
public void cleanup() {
log.info("Cleaning up...");
jda.shutdown();
jda = null;
log.info("Goodbye!");
}
public static void main(@NonNull String[] args) {
new DiscordBot();
}
}

View File

@ -1,61 +0,0 @@
package cc.restfulmc.bot.command;
import cc.restfulmc.bot.DiscordBot;
import cc.restfulmc.bot.command.impl.PlayerCommand;
import cc.restfulmc.bot.command.impl.ServerCommand;
import cc.restfulmc.sdk.client.RESTfulMCClient;
import lombok.NonNull;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Braydon
*/
public final class CommandManager extends ListenerAdapter {
/**
* The registered slash commands.
*/
private final List<SlashCommand> commands = Collections.synchronizedList(new ArrayList<>());
public CommandManager(@NonNull DiscordBot bot, @NonNull RESTfulMCClient apiClient) {
// Register commands
registerCommand(new PlayerCommand(apiClient));
registerCommand(new ServerCommand(apiClient));
// Update the commands on Discord
CommandListUpdateAction updateCommands = bot.getJda().updateCommands();
for (SlashCommand command : commands) {
updateCommands.addCommands(Commands.slash(command.getName(), command.getDescription()).addOptions(command.getOptions()));
}
updateCommands.queue();
// Handle registered events
bot.getJda().addEventListener(this);
}
@Override
public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) {
for (SlashCommand command : commands) {
if (command.getName().equals(event.getName())) {
event.deferReply().queue(); // Inform Discord we received the command
command.onExecute(event.getUser(), event.getMember(), event); // Invoke the command
break;
}
}
}
/**
* Register a slash command.
*
* @param command the command to register
*/
public void registerCommand(@NonNull SlashCommand command) {
commands.add(command);
}
}

View File

@ -1,73 +0,0 @@
package cc.restfulmc.bot.command;
import cc.restfulmc.sdk.exception.RESTfulMCAPIException;
import lombok.Getter;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
/**
* A wrapper for slash commands.
*
* @author Braydon
*/
@Getter
public abstract class SlashCommand {
/**
* The name of this command.
*/
@NonNull private final String name;
/**
* The description of this command.
*/
@NonNull private final String description;
/**
* Optional options for this command.
*/
private final OptionData[] options;
public SlashCommand(@NonNull String name, @NonNull String description, OptionData... options) {
this.name = name;
this.description = description;
this.options = options;
}
/**
* Invoked when this command is executed.
*
* @param user the executing user
* @param member the executing member, null if in dms
* @param event the event that triggered this command
*/
public abstract void onExecute(@NonNull User user, Member member, @NonNull SlashCommandInteractionEvent event);
/**
* Reply to an interaction with an API error.
*
* @param event the event to reply to
* @param apiError the api error to reply with
*/
protected final void replyWithApiError(@NonNull SlashCommandInteractionEvent event, @NonNull RESTfulMCAPIException apiError) {
replyWithGenericError(event, apiError.getCode() + " | API Error", apiError.getLocalizedMessage());
}
/**
* Reply to an interaction with a generic error.
*
* @param event the event to reply to
* @param title the title of the error
* @param description the description of the error
*/
protected final void replyWithGenericError(@NonNull SlashCommandInteractionEvent event, @NonNull String title, @NonNull String description) {
event.getHook().sendMessageEmbeds(new EmbedBuilder()
.setColor(0xAA0000)
.setTitle(title)
.setDescription(description)
.build()).queue();
}
}

View File

@ -1,72 +0,0 @@
package cc.restfulmc.bot.command.impl;
import cc.restfulmc.bot.command.SlashCommand;
import cc.restfulmc.sdk.client.RESTfulMCClient;
import cc.restfulmc.sdk.exception.RESTfulMCAPIException;
import cc.restfulmc.sdk.response.Player;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
/**
* @author Braydon
*/
@Slf4j(topic = "Server Lookup Command")
public final class PlayerCommand extends SlashCommand {
/**
* The API client to use for lookups.
*/
@NonNull private final RESTfulMCClient apiClient;
public PlayerCommand(@NonNull RESTfulMCClient apiClient) {
super("player", "Lookup a player by their username or UUID",
new OptionData(OptionType.STRING, "query", "The player username or UUID").setRequired(true)
);
this.apiClient = apiClient;
}
/**
* Invoked when this command is executed.
*
* @param user the executing user
* @param member the executing member, null if in dms
* @param event the event that triggered this command
*/
@Override
public void onExecute(@NonNull User user, Member member, @NonNull SlashCommandInteractionEvent event) {
OptionMapping query = event.getOption("query"); // Get the query
assert query != null;
String queryValue = query.getAsString(); // Get the query value
// Lookup the requested player by the given query
apiClient.async().getPlayer(queryValue).whenComplete((player, ex) -> {
// Failed to lookup the player, handle the error
if (ex != null) {
if (ex.getCause() instanceof RESTfulMCAPIException apiError) {
replyWithApiError(event, apiError);
} else { // Only print real errors
log.error("Failed fetching player:", ex);
}
return;
}
// Respond with the player
long cached = player.getCached(); // The timestamp the player was cached
event.getHook().sendMessageEmbeds(new EmbedBuilder()
.setColor(0x55FF55)
.setTitle("<:steve:1232815662599114753> Player Response", "https://api.restfulmc.cc/player/" + queryValue)
.addField("Unique ID", player.getUniqueId().toString(), true)
.addField("Username", player.getUsername(), true)
.addField("Legacy", player.isLegacy() ? "Yes" : "No", true)
.addField("Cached", cached == -1L ? "No" : "Yes, <t:" + (cached / 1000L) + ":R>", true)
.setThumbnail(player.getSkin().getParts().get(Player.SkinPart.HEAD))
.setFooter("Requested by " + user.getName() + " | " + user.getId(), user.getEffectiveAvatarUrl())
.build()).queue();
});
}
}

View File

@ -1,160 +0,0 @@
package cc.restfulmc.bot.command.impl;
import cc.restfulmc.bot.command.SlashCommand;
import cc.restfulmc.bot.common.StringUtils;
import cc.restfulmc.sdk.client.RESTfulMCClient;
import cc.restfulmc.sdk.exception.RESTfulMCAPIException;
import cc.restfulmc.sdk.response.server.BedrockMinecraftServer;
import cc.restfulmc.sdk.response.server.JavaMinecraftServer;
import cc.restfulmc.sdk.response.server.MinecraftServer;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
/**
* @author Braydon
*/
@Slf4j(topic = "Server Lookup Command")
public final class ServerCommand extends SlashCommand {
private static final Map<MinecraftServer.Platform, String> MAPPED_PLATFORM_EMOJIS = new HashMap<>() {{
put(MinecraftServer.Platform.JAVA, "<:grass_block:1232798337300828181>");
put(MinecraftServer.Platform.BEDROCK, "<:bedrock_block:1232798365427830928>");
}};
private static final DecimalFormat PLAYERS_FORMAT = new DecimalFormat("#,##0");
/**
* The API client to use for lookups.
*/
@NonNull private final RESTfulMCClient apiClient;
public ServerCommand(@NonNull RESTfulMCClient apiClient) {
super("server", "Lookup a server by its platform and hostname",
new OptionData(OptionType.STRING, "platform", "The platform: JAVA | BEDROCK").setRequired(true),
new OptionData(OptionType.STRING, "hostname", "The hostname of the server").setRequired(true)
);
this.apiClient = apiClient;
}
/**
* Invoked when this command is executed.
*
* @param user the executing user
* @param member the executing member, null if in dms
* @param event the event that triggered this command
*/
@Override
public void onExecute(@NonNull User user, Member member, @NonNull SlashCommandInteractionEvent event) {
OptionMapping platformOption = event.getOption("platform"); // The platform to query
assert platformOption != null;
String platformValue = platformOption.getAsString();
MinecraftServer.Platform platform; // The platform to lookup
try {
platform = MinecraftServer.Platform.valueOf(platformValue.toUpperCase());
} catch (IllegalArgumentException ex) {
replyWithGenericError(event, "Invalid Platform", "You must specify either **Java** or **Bedrock** as the platform.");
return;
}
OptionMapping hostname = event.getOption("hostname"); // Get the hostname to query
assert hostname != null;
String hostnameValue = hostname.getAsString(); // Get the hostname value
// Lookup the requested server by the given platform and hostname
apiClient.async().getMinecraftServer(platform, hostnameValue).whenComplete((server, ex) -> {
// Failed to lookup the server, handle the error
if (ex != null) {
if (ex.getCause() instanceof RESTfulMCAPIException apiError) {
replyWithApiError(event, apiError);
} else { // Only print real errors
log.error("Failed fetching Minecraft Server:", ex);
}
return;
}
// Respond with the server
String ip = server.getIp(); // The resolved IP of the server
MinecraftServer.GeoLocation geo = server.getGeo(); // The geo location of the server
MinecraftServer.Players players = server.getPlayers(); // The players on the server
long cached = server.getCached(); // The timestamp the server was cached
EmbedBuilder embed = new EmbedBuilder()
.setColor(0x55FF55)
.setTitle(
MAPPED_PLATFORM_EMOJIS.get(platform) + " " + StringUtils.capitalize(platform.name()) + " Server Response: " + server.getHostname(),
"https://api.restfulmc.cc/server/" + platformValue + "/" + hostnameValue
).addField("IP", (ip == null ? server.getHostname() : ip) + ":" + server.getPort(), true);
// Show Geo location if resolved
if (geo != null) {
MinecraftServer.GeoLocation.LocationData country = geo.getCountry(); // The server's country
embed.addField("Located", ":flag_" + country.getCode().toLowerCase() + ": " + country.getName(), true);
}
// Append platform specific details to the embed
if (server instanceof JavaMinecraftServer javaServer) {
appendJavaEmbed(embed, javaServer);
} else {
appendBedrockEmbed(embed, (BedrockMinecraftServer) server);
}
event.getHook().sendMessageEmbeds(embed
.addField("Players", PLAYERS_FORMAT.format(players.getOnline()) + "/" + PLAYERS_FORMAT.format(players.getMax()), true)
.addField("Cached", cached == -1L ? "No" : "Yes, <t:" + (cached / 1000L) + ":R>", true)
.setThumbnail(server instanceof JavaMinecraftServer javaServer ? javaServer.getFavicon().getUrl() : "https://api.restfulmc.cc/server/icon/invalid")
.setFooter("Requested by " + user.getName() + " | " + user.getId(), user.getEffectiveAvatarUrl())
.build()).queue();
});
}
/**
* Append Java specific details to an embed.
*
* @param embed the embed to append to
* @param javaServer the java server
*/
private void appendJavaEmbed(@NonNull EmbedBuilder embed, @NonNull JavaMinecraftServer javaServer) {
JavaMinecraftServer.Version version = javaServer.getVersion(); // The version of the server
String protocolName = version.getProtocolName(); // The name of the server's version protocol
String platform = version.getPlatform(); // The server's platform
String world = javaServer.getWorld(); // The server's main world
JavaMinecraftServer.Plugin[] plugins = javaServer.getPlugins(); // The server's plugins
embed.addField("Version", version.getProtocol() + (protocolName == null ? "" : " (" + protocolName + ")"), true);
if (platform != null) {
embed.addField("Platform", platform, true);
}
if (world != null) {
embed.addField("World", world, true);
}
if (plugins != null) {
embed.addField("Plugins", String.valueOf(plugins.length), true);
}
embed.addField("Query", javaServer.isQueryEnabled() ? "Enabled" : "Disabled", true);
embed.addField("Mojang Banned", javaServer.isMojangBanned() ? "Yes" : "No", true);
}
/**
* Append Bedrock specific details to an embed.
*
* @param embed the embed to append to
* @param bedrockServer the bedrock server
*/
private void appendBedrockEmbed(@NonNull EmbedBuilder embed, @NonNull BedrockMinecraftServer bedrockServer) {
BedrockMinecraftServer.Version version = bedrockServer.getVersion(); // The version of the server
BedrockMinecraftServer.GameMode gamemode = bedrockServer.getGamemode(); // The game mode of the server
embed.addField("Version", version.getProtocol() + " (" + version.getName() + ")", true);
embed.addField("Edition", bedrockServer.getEdition().name(), true);
embed.addField("GameMode", gamemode.getName() + " (" + gamemode.getNumericId() + ")", true);
}
}

View File

@ -1,21 +0,0 @@
package cc.restfulmc.bot.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
/**
* @author Braydon
*/
@UtilityClass
public final class StringUtils {
/**
* Capitalize the first character in the given string.
*
* @param input the input to capitalize
* @return the capitalized string
*/
@NonNull
public static String capitalize(@NonNull String input) {
return Character.toUpperCase(input.charAt(0)) + input.substring(1).toLowerCase();
}
}

View File

@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
}
}

8
Frontend/.gitignore vendored
View File

@ -1,8 +1,6 @@
node_modules
.idea/
.vscode/
.VSCodeCounter/
.next/
.fleet/
.vscode/
.env*.local
next-env.d.ts
.sentryclirc
next-env.d.ts

View File

@ -1,11 +1,11 @@
FROM imbios/bun-node AS base
FROM fascinated/docker-images:nodejs_20_with_yarn AS base
# Install dependencies
FROM base AS depends
WORKDIR /usr/src/app
COPY package.json* bun.lockb* ./
RUN bun install --frozen-lockfile --quiet
COPY package.json* yarn.lock* ./
RUN yarn install --frozen-lockfile --quiet
# Build the app
@ -14,15 +14,15 @@ WORKDIR /usr/src/app
COPY --from=depends /usr/src/app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN bun run build
RUN yarn run build
# Run the app
FROM base AS runner
WORKDIR /usr/src/app
RUN addgroup --system --gid 1007 nextjs
RUN adduser --system --uid 1007 nextjs
RUN addgroup --system --gid 1001 nextjs
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next
RUN chown nextjs:nextjs .next
@ -30,7 +30,6 @@ RUN chown nextjs:nextjs .next
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/.next ./.next
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/docs ./docs
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/next.config.mjs ./next.config.mjs
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/package.json ./package.json
@ -43,4 +42,4 @@ EXPOSE 80
ENV PORT 80
USER nextjs
CMD ["bun", "start"]
CMD ["yarn", "start"]

Some files were not shown because too many files have changed in this diff Show More