diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 72a3879..8259339 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -3,6 +3,9 @@ plugins { } dependencies { + implementation(project(":web-api")) // Web-API + compileOnly("org.projectlombok:lombok:1.18.46") + annotationProcessor("org.projectlombok:lombok:1.18.46") // Cosmos compileOnly("com.alttd.cosmos:cosmos-api:1.21.8-R0.1-SNAPSHOT") { isChanging = true diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java index 6fd44d0..6c739f1 100644 --- a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLog.java @@ -1,6 +1,9 @@ package com.alttd.chat.objects.chat_log; +import com.alttd.altitudeweb.model.ChatMessageDto; import com.alttd.chat.objects.BatchInsertable; +import lombok.AllArgsConstructor; +import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.jetbrains.annotations.NotNull; @@ -11,24 +14,21 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.UUID; +@AllArgsConstructor public class ChatLog implements BatchInsertable { + @Getter private final UUID uuid; + @Getter private final Instant timestamp; private final String server; + @Getter private final String message; + @Getter private final Component miniMessage; + @Getter private final boolean blocked; - protected ChatLog(UUID uuid, Instant timestamp, String server, String message, Component miniMessage, boolean blocked) { - this.uuid = uuid; - this.timestamp = timestamp; - this.server = server; - this.message = message; - this.miniMessage = miniMessage; - this.blocked = blocked; - } - @Override public void prepareStatement(@NotNull PreparedStatement preparedStatement) throws SQLException { preparedStatement.setString(1, uuid.toString()); @@ -41,19 +41,17 @@ public class ChatLog implements BatchInsertable { preparedStatement.setInt(6, blocked ? 1 : 0); } - public UUID getUuid() { - return uuid; - } - - public Instant getTimestamp() { - return timestamp; - } - - public String getMessage() { - return message; - } - - public boolean isBlocked() { - return blocked; + public ChatMessageDto toDto() { + ChatMessageDto chatMessageDto = new ChatMessageDto(); + if (miniMessage == null) { + throw new IllegalArgumentException("MiniMessage cannot be null"); + } + chatMessageDto.setMessage(GsonComponentSerializer.gson().serialize(miniMessage)); + chatMessageDto.setChannel(ChatMessageDto.ChannelEnum.CHAT); + chatMessageDto.setServer(server); + chatMessageDto.setTimestamp(timestamp); + chatMessageDto.setUuid(uuid); + chatMessageDto.setBlocked(blocked); + return chatMessageDto; } } diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java index 55281bf..f551262 100644 --- a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogHandler.java @@ -16,6 +16,7 @@ import java.util.concurrent.*; public class ChatLogHandler { + private final ChatLogWebHandler chatLogWebHandler = new ChatLogWebHandler(); private static ChatLogHandler instance = null; private ScheduledExecutorService executorService = null; @@ -123,7 +124,9 @@ public class ChatLogHandler { } public void addChatLog(UUID uuid, String server, String message, Component miniMessage, boolean blocked) { - addLog(new ChatLog(uuid, Instant.now(), server, message, miniMessage, blocked)); + ChatLog chatLog = new ChatLog(uuid, Instant.now(), server, message, miniMessage, blocked); + addLog(chatLog); + chatLogWebHandler.forwardChatLogToWeb(chatLog); } public CompletableFuture> retrieveChatLogs(UUID uuid, Duration duration, String server) { diff --git a/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java new file mode 100644 index 0000000..a562db1 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/objects/chat_log/ChatLogWebHandler.java @@ -0,0 +1,96 @@ +package com.alttd.chat.objects.chat_log; + +import com.alttd.altitudeweb.api.ChatApi; +import com.alttd.altitudeweb.invoker.ApiClient; +import com.alttd.altitudeweb.invoker.ApiException; +import com.alttd.altitudeweb.model.ChatMessageDto; +import com.alttd.chat.util.ALogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ChatLogWebHandler { + + private static final int MAX_QUEUE_SIZE = 100; + private static final Logger log = LoggerFactory.getLogger(ChatLogWebHandler.class); + + private final Queue queue = new ConcurrentLinkedQueue<>(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + private final ChatApi chatApi; + + public ChatLogWebHandler() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath("http://10.0.0.109"); + + chatApi = new ChatApi(apiClient); + } + + private volatile boolean sending; + + public void forwardChatLogToWeb(ChatLog chatLog) { + if (queue.size() >= MAX_QUEUE_SIZE) { + queue.clear(); + log.error("Chat log queue overflow, is the web backend still running?"); + } + + queue.add(chatLog); + triggerSend(); + } + + private void triggerSend() { + if (sending) { + return; + } + + synchronized (this) { + if (sending || queue.isEmpty()) { + return; + } + + sending = true; + } + + executor.execute(this::sendBatch); + } + + private void sendBatch() { + try { + ArrayList chatLogList = new ArrayList<>(); + + ChatLog log; + while ((log = queue.poll()) != null) { + if (log.getMiniMessage() == null) { + ALogger.warn("No mini message for message, skipping"); + continue; + } + chatLogList.add(log); + } + + List batch = chatLogList.stream().map(ChatLog::toDto).toList(); + + if (!chatLogList.isEmpty()) { + try { + chatApi.sendChatMessages(batch); + } catch (ApiException e) { + ALogger.error("Failed to send chat messages to web backend, " + + "adding messages back to queue for another try", e + ); + queue.addAll(chatLogList); + } + } + } finally { + synchronized (this) { + sending = false; + } + + triggerSend(); + } + } +} diff --git a/galaxy/build.gradle.kts b/galaxy/build.gradle.kts index 7b30111..f8cbc05 100644 --- a/galaxy/build.gradle.kts +++ b/galaxy/build.gradle.kts @@ -11,6 +11,8 @@ dependencies { compileOnly("com.alttd.cosmos:cosmos-api:1.21.8-R0.1-SNAPSHOT") { isChanging = true } + compileOnly("org.projectlombok:lombok:1.18.46") + annotationProcessor("org.projectlombok:lombok:1.18.46") compileOnly("com.gitlab.ruany:LiteBansAPI:0.6.1") // move to proxy compileOnly("org.apache.commons:commons-lang3:3.17.0") // needs an alternative, already removed from upstream api and will be removed in server compileOnly("net.luckperms:api:5.5") // Luckperms diff --git a/settings.gradle.kts b/settings.gradle.kts index b04783d..33ca2eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,3 +35,5 @@ pluginManagement { gradlePluginPortal() } } + +include("web-api") \ No newline at end of file diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 411a119..e756e5c 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -5,6 +5,8 @@ plugins { dependencies { implementation(project(":api")) // API + compileOnly("org.projectlombok:lombok:1.18.46") + annotationProcessor("org.projectlombok:lombok:1.18.46") compileOnly("com.velocitypowered:velocity-api:3.5.0-SNAPSHOT") annotationProcessor("com.velocitypowered:velocity-api:3.5.0-SNAPSHOT") implementation("mysql:mysql-connector-java:8.0.33") // mysql diff --git a/web-api/build.gradle.kts b/web-api/build.gradle.kts new file mode 100644 index 0000000..e6c5707 --- /dev/null +++ b/web-api/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + java + id("org.openapi.generator") version "7.12.0" +} + +group = "com.alttd.chat" +version = "2.0.0-SNAPSHOT" + +dependencies { + implementation("io.swagger.core.v3:swagger-annotations:2.2.28") + + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + implementation("com.google.code.gson:gson:2.12.1") + implementation("io.gsonfire:gson-fire:1.9.0") + + implementation("javax.annotation:javax.annotation-api:1.3.2") + implementation("jakarta.annotation:jakarta.annotation-api:2.1.1") +} + +sourceSets { + main { + java { + srcDir("${projectDir}/build/generated-sources/client/src/main/java") + } + } +} + +tasks.named("openApiGenerate") { + generatorName.set("java") + inputSpec.set("${projectDir}/src/main/resources/chat-api.yml") + configFile.set("${projectDir}/src/main/resources/config_backend.json") + outputDir.set("${projectDir}/build/generated-sources/client") + + apiPackage.set("com.alttd.altitudeweb.api") + modelPackage.set("com.alttd.altitudeweb.model") + + modelNameSuffix.set("Dto") + + additionalProperties.set( + mapOf( + "dateLibrary" to "java8", + "library" to "okhttp-gson" + ) + ) + + typeMappings.set(mapOf("OffsetDateTime" to "Instant")) + importMappings.set(mapOf("OffsetDateTime" to "java.time.Instant")) + + generateApiTests.set(false) + generateApiDocumentation.set(false) + generateModelTests.set(false) + generateModelDocumentation.set(false) + + generateAliasAsModel.set(true) +} + +sourceSets { + main { + java { + srcDir(layout.buildDirectory.dir("generated/openapi/src/main/java")) + } + } +} + +tasks.compileJava { + dependsOn("openApiGenerate") +} diff --git a/web-api/src/main/resources/chat-api.yml b/web-api/src/main/resources/chat-api.yml new file mode 100644 index 0000000..35b273d --- /dev/null +++ b/web-api/src/main/resources/chat-api.yml @@ -0,0 +1,135 @@ +openapi: 3.0.3 + +info: + title: Minecraft Network API + version: 1.0.0 + +servers: + - url: http://localhost:8080/api + +tags: + - name: chat + description: Data for displaying Chat messages to clients + +paths: + /chat/send/chat/message: + post: + tags: + - chat + summary: Sends one or more chat messages + operationId: sendChatMessages + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ChatMessage" + responses: + "202": + description: Accepted + + /chat/send/servers/state: + post: + tags: + - chat + summary: Update server states + operationId: updateServerStates + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ServerState" + responses: + "202": + description: Accepted + +components: + schemas: + User: + type: object + required: + - uuid + - name + - styledName + + properties: + uuid: + type: string + format: uuid + name: + type: string + styledName: + type: string + + ChatMessage: + type: object + required: + - uuid + - message + - channel + - server + - timestamp + - blocked + properties: + uuid: + type: string + format: uuid + channel: + type: string + enum: + - CHAT + - PARTY + - AC + - GAC + message: + type: string + server: + type: string + timestamp: + type: string + format: date-time + blocked: + type: boolean + + PrivateMessage: + type: object + required: + - sender + - receiver + - message + - senderServer + - receiverServer + properties: + sender: + type: string + format: uuid + receiver: + type: string + format: uuid + message: + type: string + senderServer: + type: string + receiverServer: + type: string + + Server: + type: object + properties: + name: + type: string + players: + type: array + items: + $ref: "#/components/schemas/User" + + ServerState: + type: object + properties: + servers: + type: array + items: + $ref: "#/components/schemas/Server" diff --git a/web-api/src/main/resources/config_backend.json b/web-api/src/main/resources/config_backend.json new file mode 100644 index 0000000..e6e6d46 --- /dev/null +++ b/web-api/src/main/resources/config_backend.json @@ -0,0 +1,22 @@ +{ + "library": "okhttp-gson", + "hideGenerationTimestamp": true, + "modelPackage": "com.alttd.altitudeweb.model", + "apiPackage": "com.alttd.altitudeweb.api", + "invokerPackage": "com.alttd.altitudeweb.invoker", + "serializableModel": true, + "openApiNullable": false, + "useTags": true, + "generateApis": true, + "generateApiTests": false, + "generateApiDocumentation": false, + "generateModels": true, + "generateModelTests": false, + "generateSupportingFiles": true, + "modelNameSuffix": "Dto", + "generateTests": false, + "dateLibrary": "java8", + "enumUnknownDefaultCase": true, + "disallowAdditionalPropertiesIfNotPresent": false, + "useJakartaEe": true +}