Add notification server and file download service implementation

- Introduce `NotificationServer` for handling HTTP notifications.
- Register `ProxyShutdownEvent` to stop the notification server on shutdown.
- Add `FileDownloadService` for asynchronous file downloads using the configured endpoint.
- Update `Config` to include `download-endpoint`.
- Add Javalin and SLF4J dependencies for the HTTP server.
This commit is contained in:
akastijn 2025-06-23 23:06:47 +02:00
parent df8f08544b
commit d4359bf480
5 changed files with 174 additions and 0 deletions

View File

@ -18,6 +18,8 @@ dependencies {
compileOnly("com.gitlab.ruany:LiteBansAPI:0.6.1")
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
implementation("io.javalin:javalin:6.6.0") // Javalin for HTTP server
implementation("org.slf4j:slf4j-api:2.0.9") // Required by Javalin
}
tasks.test {

View File

@ -3,10 +3,12 @@ package com.alttd.webinterface;
import com.alttd.webinterface.commands.Login;
import com.alttd.webinterface.commands.Reload;
import com.alttd.webinterface.config.Config;
import com.alttd.webinterface.http.NotificationServer;
import com.alttd.webinterface.listeners.ProxyPlayerListener;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.extern.slf4j.Slf4j;
@ -18,6 +20,8 @@ import lombok.extern.slf4j.Slf4j;
)
public class WebInterface {
private final ProxyServer server;
private NotificationServer notificationServer;
private static final int HTTP_PORT = 8080; // Default port, could be made configurable
@Inject
public WebInterface(ProxyServer proxyServer) {
@ -29,11 +33,36 @@ public class WebInterface {
reloadConfig();
server.getEventManager().register(this, new ProxyPlayerListener());
loadCommands();
startNotificationServer();
}
@Subscribe
public void onProxyShutdown(ProxyShutdownEvent event) {
stopNotificationServer();
}
private void startNotificationServer() {
notificationServer = new NotificationServer(HTTP_PORT);
notificationServer.startServer();
log.info("Started notification server on port {}", HTTP_PORT);
}
private void stopNotificationServer() {
if (notificationServer != null) {
notificationServer.stopServer();
log.info("Stopped notification server");
}
}
public void reloadConfig() {
Config.init();
log.info("Reloaded WebInterface config.");
// Restart the notification server to apply any configuration changes
if (notificationServer != null) {
stopNotificationServer();
}
startNotificationServer();
}
public void loadCommands() {// all (proxy)commands go here

View File

@ -201,6 +201,7 @@ public final class Config {
public static String LOGIN_CODE_ENDPOINT = "https://alttd.com/login/requestNewUserLogin/<uuid>";
public static String LOGIN_ENDPOINT = "https://alttd.com/login";
public static String DOWNLOAD_ENDPOINT = "https://alttd.com/particles/download";
public static String SECRET = "";
private static void site() {
if (SECRET.isEmpty()) {
@ -210,6 +211,7 @@ public final class Config {
}
SECRET = getString("secret", SECRET);
LOGIN_ENDPOINT = getString("login-endpoint", LOGIN_ENDPOINT);
DOWNLOAD_ENDPOINT = getString("download-endpoint", DOWNLOAD_ENDPOINT);
LOGIN_CODE_ENDPOINT = getString("login-code-endpoint", LOGIN_CODE_ENDPOINT);
}
}

View File

@ -0,0 +1,83 @@
package com.alttd.webinterface.http;
import com.alttd.webinterface.web_interact.FileDownloadService;
import io.javalin.Javalin;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
/**
* HTTP server that exposes the /notify/<file>.json endpoint.
*/
@Slf4j
public class NotificationServer {
private final int port;
private Javalin app;
/**
* Creates a new NotificationServer that listens on the specified port.
*
* @param port The port to listen on
*/
public NotificationServer(int port) {
this.port = port;
}
/**
* Starts the server.
*/
public void startServer() {
try {
app = Javalin.create(config -> {
config.showJavalinBanner = false;
}).start(port);
app.get("/notify/{file}.json", this::handleNotifyRequest);
log.info("NotificationServer started on port {}", port);
} catch (Exception e) {
log.error("Failed to start NotificationServer", e);
}
}
/**
* Stops the server.
*/
public void stopServer() {
if (app != null) {
app.stop();
log.info("NotificationServer stopped");
}
}
/**
* Handles requests to the /notify/<file>.json endpoint.
*/
private void handleNotifyRequest(Context ctx) {
String uri = ctx.path();
log.info("Received request: {} {}", ctx.method(), uri);
String fileName = ctx.pathParam("file") + ".json";
log.info("Requested file: {}", fileName);
FileDownloadService.downloadFileAsync(fileName).thenAccept(fileData -> {
if (fileData.isPresent()) {
ctx.contentType("application/json");
ctx.result(fileData.get());
} else {
ctx.status(HttpStatus.NOT_FOUND);
ctx.contentType("text/plain");
ctx.result("File not found or download failed");
}
}).exceptionally(e -> {
log.error("Error downloading file: {}", fileName, e);
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR);
ctx.contentType("text/plain");
ctx.result("Failed to handle download");
return null;
});
}
}

View File

@ -0,0 +1,58 @@
package com.alttd.webinterface.web_interact;
import com.alttd.webinterface.config.Config;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Service for downloading files from the configured download endpoint.
*/
@Slf4j
public class FileDownloadService {
/**
* Asynchronously downloads a file from the configured download endpoint.
*
* @param fileName The name of the file to download
* @return A CompletableFuture containing an Optional with the file content if successful,
* or an empty Optional otherwise
*/
public static CompletableFuture<Optional<byte[]>> downloadFileAsync(String fileName) {
String downloadUrl = Config.DOWNLOAD_ENDPOINT;
if (!downloadUrl.endsWith("/")) {
downloadUrl += "/";
}
downloadUrl += fileName;
log.debug("Downloading file from {}", downloadUrl);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(downloadUrl))
.header("Authorization", "SECRET " + Config.SECRET)
.GET()
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(response -> {
if (response.statusCode() == HttpServletResponse.SC_OK) {
log.debug("Successfully downloaded file: {}", fileName);
return Optional.of(response.body());
} else {
log.error("Failed to download file: {}. Status code: {}", fileName, response.statusCode());
return Optional.<byte[]>empty();
}
})
.exceptionally(e -> {
log.error("Exception occurred while downloading file: {}", fileName, e);
return Optional.empty();
});
}
}