diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..688b074
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# Project exclude paths
+/target/
+/StatTrack/
+/.idea/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..caf4e17
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+ net.michel
+ StatTrack
+ 1.0-SNAPSHOT
+
+
+ 17
+ 17
+
+
+
+
+ io.javalin
+ javalin
+ 4.6.1
+
+
+ org.json
+ json
+ 20220320
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+
+
+ commons-io
+ commons-io
+ 2.11.0
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.3
+
+
+
+
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..21fa7d3
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,2 @@
+# StatTrack (WIP)
+It's a simple tool to track your minecraft server status.
\ No newline at end of file
diff --git a/src/main/java/net/michel/stattrack/StatTrack.java b/src/main/java/net/michel/stattrack/StatTrack.java
new file mode 100644
index 0000000..229324d
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/StatTrack.java
@@ -0,0 +1,83 @@
+package net.michel.stattrack;
+
+import io.javalin.Javalin;
+import io.javalin.http.staticfiles.Location;
+import io.javalin.http.util.RateLimiter;
+import lombok.Getter;
+import lombok.Setter;
+import net.michel.stattrack.api.v1.ServerApi;
+import net.michel.stattrack.config.Config;
+import net.michel.stattrack.objects.Server;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static io.javalin.apibuilder.ApiBuilder.get;
+import static io.javalin.apibuilder.ApiBuilder.path;
+
+@Getter
+@Setter
+public class StatTrack {
+ public static StatTrack instance;
+ private Config config;
+ private Javalin javalin;
+ private final ArrayList servers = new ArrayList<>();
+
+ public void init() {
+ instance = this;
+ this.config = new Config();
+ this.javalin = Javalin.create(config -> {
+ config.addStaticFiles("/public", Location.CLASSPATH);
+ });
+ }
+
+ public void start() {
+ System.out.println("Loading config...");
+ config.init();
+
+ System.out.println("Starting server...");
+
+ var rateLimit = new RateLimiter(TimeUnit.MINUTES);
+
+ javalin.before(ctx -> {
+ if (ctx.path().startsWith("/api")) rateLimit.incrementCounter(ctx, 15);
+ });
+
+ javalin.routes(() -> {
+ path("/api/v1", () -> {
+ get("serverlist", ServerApi::getServerList);
+ get("addserver", ServerApi::addServerToList);
+ get("updateserver", ServerApi::serverUpdate);
+ get("serverinfo", ServerApi::serverInfo);
+ });
+ });
+
+ javalin.start(config.getPort());
+
+ Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
+ servers.forEach(server -> {
+ if (System.currentTimeMillis() - server.getLastUpdate() > TimeUnit.MINUTES.toMillis(10)) {
+ server.setOnline(false);
+ System.err.println("Server " + server.getName() + " is not responding");
+ //todo: send a webhook to discord
+ }
+ });
+ }, 5, 15, TimeUnit.MINUTES);
+ }
+
+
+ public boolean serverExists(String name) {
+ for (Server server : servers) {
+ if (server.getName().equals(name)) return true;
+ }
+ return false;
+ }
+
+ public Server getServerByName(String name) {
+ for (Server server : servers) {
+ if (server.getName().equals(name)) return server;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/net/michel/stattrack/api/v1/ServerApi.java b/src/main/java/net/michel/stattrack/api/v1/ServerApi.java
new file mode 100644
index 0000000..c6bb4c8
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/api/v1/ServerApi.java
@@ -0,0 +1,134 @@
+package net.michel.stattrack.api.v1;
+
+import io.javalin.http.Context;
+import net.michel.stattrack.StatTrack;
+import net.michel.stattrack.objects.Server;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class ServerApi {
+
+ /**
+ * Get a list of all servers
+ *
+ * @param ctx The context of the request.
+ */
+ public static void getServerList(Context ctx) {
+ var servers = StatTrack.instance.getServers();
+
+ if (servers.size() == 0) {
+ responseError(ctx, "server list is empty");
+ return;
+ }
+
+ JSONObject json = new JSONObject();
+ JSONArray array = new JSONArray();
+ StatTrack.instance.getServers().forEach(server -> array.put(server.getName()));
+ json.put("servers", array);
+
+ ctx.json(json.toString());
+ }
+
+ /**
+ * Adds a server to the list of servers.
+ *
+ * @param ctx The context of the request.
+ */
+ public static void addServerToList(Context ctx) {
+ if (checkKey(ctx)) {
+ ctx.status(401);
+ return;
+ }
+
+ String name = ctx.queryParamAsClass("name", String.class).getOrDefault(null);
+ if (name == null || name.isEmpty()) {
+ responseError(ctx, "name is required");
+ return;
+ }
+
+ if (StatTrack.instance.serverExists(name)) {
+ responseError(ctx, "server already exists");
+ return;
+ }
+
+ JSONObject json = new JSONObject();
+ json.put("success", true);
+ json.put("name", name);
+
+ var server = new Server(name, true, 0, 0, 0, System.currentTimeMillis());
+ StatTrack.instance.getServers().add(server);
+ ctx.result(json.toString());
+ }
+
+ /**
+ * Updates the server status
+ *
+ * @param ctx The context of the request.
+ */
+ public static void serverUpdate(Context ctx) {
+ if (checkKey(ctx)) {
+ ctx.status(401);
+ return;
+ }
+
+ String name = ctx.queryParamAsClass("name", String.class).getOrDefault(null);
+ if (name == null || name.isEmpty()) {
+ responseError(ctx, "name is required");
+ return;
+ }
+
+ Boolean online = ctx.queryParamAsClass("online", Boolean.class).getOrDefault(false);
+ Integer players = ctx.queryParamAsClass("players", Integer.class).getOrDefault(0);
+ Integer maxPlayers = ctx.queryParamAsClass("maxPlayers", Integer.class).getOrDefault(0);
+ Integer ping = ctx.queryParamAsClass("ping", Integer.class).getOrDefault(0);
+
+ var server = StatTrack.instance.getServerByName(name);
+ if (server == null) {
+ responseError(ctx, "server does not exist");
+ return;
+ }
+
+ server.updateServer(online, players, maxPlayers, ping);
+
+ JSONObject json = new JSONObject();
+ json.put("success", true);
+ json.put("name", name);
+ ctx.result(json.toString());
+ }
+
+ private static boolean checkKey(Context ctx) {
+ String key = ctx.queryParamAsClass("key", String.class).getOrDefault(null);
+ if (key == null || !key.equals(StatTrack.instance.getConfig().getSecretKey())) {
+ return true;
+ }
+ return false;
+ }
+
+ private static void responseError(Context ctx, String error) {
+ JSONObject json = new JSONObject();
+ json.put("error", error);
+ ctx.result(json.toString());
+ }
+
+ public static void serverInfo(Context context) {
+ String name = context.queryParamAsClass("name", String.class).getOrDefault(null);
+ if (name == null || name.isEmpty()) {
+ responseError(context, "name is required");
+ return;
+ }
+
+ var server = StatTrack.instance.getServerByName(name);
+ if (server == null) {
+ responseError(context, "server does not exist");
+ return;
+ }
+
+ JSONObject json = new JSONObject();
+ json.put("name", server.getName());
+ json.put("online", server.isOnline());
+ json.put("players", server.getPlayers());
+ json.put("maxPlayers", server.getMaxPlayers());
+ json.put("ping", server.getPing());
+ context.result(json.toString());
+ }
+}
diff --git a/src/main/java/net/michel/stattrack/config/Config.java b/src/main/java/net/michel/stattrack/config/Config.java
new file mode 100644
index 0000000..c51bacc
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/config/Config.java
@@ -0,0 +1,58 @@
+package net.michel.stattrack.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import net.michel.stattrack.utils.StringUtils;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Objects;
+
+@Getter
+@Setter
+public class Config {
+ private JSONObject json;
+ private File configFile;
+ private String secretKey;
+ private int port;
+
+ public void init() {
+ try {
+ configFile = new File("StatTrack/config.json");
+ if (!configFile.exists()) {
+ configFile.getParentFile().mkdirs();
+ Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/config.json")), configFile.toPath());
+ }
+
+ InputStream inputStream = new FileInputStream(configFile);
+ String jsonTxt = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+ json = new JSONObject(jsonTxt);
+
+
+ this.secretKey = json.getString("secretKey");
+ if (this.secretKey.isEmpty()) {
+ this.secretKey = StringUtils.generateRandomString(32);
+ saveField("secretKey", this.secretKey);
+ }
+
+ this.port = json.getInt("port");
+ if (this.port == 0) {
+ this.port = 8080;
+ saveField("port", this.port);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void saveField(String key, Object value) throws IOException {
+ json.put(key, value);
+ Files.write(configFile.toPath(), json.toString(4).getBytes());
+ }
+}
diff --git a/src/main/java/net/michel/stattrack/main/Bootstrap.java b/src/main/java/net/michel/stattrack/main/Bootstrap.java
new file mode 100644
index 0000000..02b112c
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/main/Bootstrap.java
@@ -0,0 +1,13 @@
+package net.michel.stattrack.main;
+
+import net.michel.stattrack.StatTrack;
+
+public class Bootstrap {
+
+ public static void main(String[] args) {
+ StatTrack statTrack = new StatTrack();
+ statTrack.init();
+ statTrack.start();
+ }
+
+}
diff --git a/src/main/java/net/michel/stattrack/objects/Server.java b/src/main/java/net/michel/stattrack/objects/Server.java
new file mode 100644
index 0000000..61d708b
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/objects/Server.java
@@ -0,0 +1,25 @@
+package net.michel.stattrack.objects;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class Server {
+ private String name;
+ private boolean online;
+ private int players;
+ private int maxPlayers;
+ private int ping;
+ private long lastUpdate;
+
+ public void updateServer(boolean online, int players, int maxPlayers, int ping) {
+ this.online = online;
+ this.players = players;
+ this.maxPlayers = maxPlayers;
+ this.ping = ping;
+ this.lastUpdate = System.currentTimeMillis();
+ }
+}
diff --git a/src/main/java/net/michel/stattrack/utils/StringUtils.java b/src/main/java/net/michel/stattrack/utils/StringUtils.java
new file mode 100644
index 0000000..8ac7e43
--- /dev/null
+++ b/src/main/java/net/michel/stattrack/utils/StringUtils.java
@@ -0,0 +1,14 @@
+package net.michel.stattrack.utils;
+
+public class StringUtils {
+
+ public static String generateRandomString(int length) {
+ String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ int index = (int) (Math.random() * characters.length());
+ result.append(characters.charAt(index));
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/resources/config.json b/src/main/resources/config.json
new file mode 100644
index 0000000..005d091
--- /dev/null
+++ b/src/main/resources/config.json
@@ -0,0 +1,4 @@
+{
+ "secretKey": "",
+ "port": 0
+}
\ No newline at end of file
diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html
new file mode 100644
index 0000000..8ca86bc
--- /dev/null
+++ b/src/main/resources/public/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ StatTrack
+
+
+hi
+
+
\ No newline at end of file