Compare commits

...

2 Commits

12 changed files with 455 additions and 75 deletions

View File

@ -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

View File

@ -15,15 +15,22 @@ import java.util.concurrent.CompletionException;
public class ChatLogQueries {
protected static void createChatLogTable() {
String nicknamesTableQuery = "CREATE TABLE IF NOT EXISTS chat_log("
+ "uuid CHAR(48) NOT NULL,"
+ "time_stamp TIMESTAMP(6) NOT NULL, "
+ "server VARCHAR(50) NOT NULL, "
+ "chat_message VARCHAR(300) NOT NULL, "
+ "blocked BIT(1) NOT NULL DEFAULT 0"
+ ")";
String nicknamesTableQuery = """
CREATE TABLE chat_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL,
time_stamp TIMESTAMP(6) NOT NULL,
server VARCHAR(50) NOT NULL,
chat_message VARCHAR(300) NOT NULL,
mini_message JSON,
blocked BOOLEAN NOT NULL DEFAULT FALSE,
try (PreparedStatement preparedStatement = DatabaseConnection.getConnection().prepareStatement(nicknamesTableQuery)) {
INDEX idx_time_stamp (time_stamp)
)
""";
try (PreparedStatement preparedStatement = DatabaseConnection.getConnection().prepareStatement(
nicknamesTableQuery)) {
preparedStatement.executeUpdate();
} catch (Throwable throwable) {
ALogger.error("Failed to create chat log table", throwable);
@ -31,7 +38,7 @@ public class ChatLogQueries {
}
public static @NotNull CompletableFuture<Boolean> storeMessages(HashMap<UUID, List<ChatLog>> chatMessages) {
String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, blocked) VALUES (?, ?, ?, ?, ?)";
String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, mini_message, blocked) VALUES (?, ?, ?, ?, ?, ?)";
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = DatabaseConnection.createTransactionConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
@ -76,7 +83,9 @@ public class ChatLogQueries {
return chatLogs;
} catch (SQLException sqlException) {
ALogger.error(String.format("Failed to retrieve messages for user %s", uuid), sqlException);
throw new CompletionException(String.format("Failed to retrieve messages for user %s", uuid), sqlException);
throw new CompletionException(String.format("Failed to retrieve messages for user %s", uuid),
sqlException
);
}
});
}
@ -90,8 +99,13 @@ public class ChatLogQueries {
preparedStatement.setTimestamp(1, Timestamp.from(Instant.now().minus(duration)));
return preparedStatement.execute();
} catch (SQLException sqlException) {
ALogger.error(String.format("Failed to delete messages older than %s days", duration.toDays()), sqlException);
throw new CompletionException(String.format("Failed to delete messages older than %s days", duration.toDays()), sqlException);
ALogger.error(String.format("Failed to delete messages older than %s days", duration.toDays()),
sqlException
);
throw new CompletionException(String.format("Failed to delete messages older than %s days",
duration.toDays()
), sqlException
);
}
});
}

View File

@ -1,6 +1,11 @@
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;
import java.sql.PreparedStatement;
@ -9,44 +14,44 @@ 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, boolean blocked) {
this.uuid = uuid;
this.timestamp = timestamp;
this.server = server;
this.message = message;
this.blocked = blocked;
}
@Override
public void prepareStatement(@NotNull PreparedStatement preparedStatement) throws SQLException {
preparedStatement.setString(1, uuid.toString());
preparedStatement.setTimestamp(2, Timestamp.from(timestamp));
preparedStatement.setString(3, server);
preparedStatement.setString(4, message);
preparedStatement.setInt(5, blocked ? 1 : 0);
preparedStatement.setString(5,
miniMessage == null ? null : GsonComponentSerializer.gson().serialize(miniMessage)
);
preparedStatement.setInt(6, blocked ? 1 : 0);
}
public UUID getUuid() {
return uuid;
public ChatMessageDto toDto() {
ChatMessageDto chatMessageDto = new ChatMessageDto();
if (miniMessage == null) {
throw new IllegalArgumentException("MiniMessage cannot be null");
}
public Instant getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public boolean isBlocked() {
return blocked;
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;
}
}

View File

@ -3,6 +3,8 @@ package com.alttd.chat.objects.chat_log;
import com.alttd.chat.config.Config;
import com.alttd.chat.database.ChatLogQueries;
import com.alttd.chat.util.ALogger;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.sql.ResultSet;
@ -14,12 +16,14 @@ import java.util.concurrent.*;
public class ChatLogHandler {
private final ChatLogWebHandler chatLogWebHandler = new ChatLogWebHandler();
private static ChatLogHandler instance = null;
private ScheduledExecutorService executorService = null;
public static ChatLogHandler getInstance(boolean enableLogging) {
if (instance == null)
if (instance == null) {
instance = new ChatLogHandler(enableLogging);
}
return instance;
}
@ -35,23 +39,30 @@ public class ChatLogHandler {
Duration deleteThreshold = Duration.ofDays(Config.CHAT_LOG_DELETE_OLDER_THAN_DAYS);
ChatLogQueries.deleteOldMessages(deleteThreshold).thenAccept(success -> {
if (success) {
ALogger.info(String.format("Deleted all messages older than %s days from chat log database.", deleteThreshold.toDays()));
ALogger.info(String.format("Deleted all messages older than %s days from chat log database.",
deleteThreshold.toDays()
));
} else {
ALogger.warn(String.format("Failed to delete all messages older than %s days from chat log database.", deleteThreshold.toDays()));
ALogger.warn(String.format("Failed to delete all messages older than %s days from chat log database.",
deleteThreshold.toDays()
));
}
});
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(() -> {
saveToDatabase(false);
ALogger.info(String.format("Running scheduler to save messages with a %d delay", Config.CHAT_LOG_SAVE_DELAY_MINUTES));
ALogger.info(String.format("Running scheduler to save messages with a %d delay",
Config.CHAT_LOG_SAVE_DELAY_MINUTES
));
},
Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES);
Config.CHAT_LOG_SAVE_DELAY_MINUTES, Config.CHAT_LOG_SAVE_DELAY_MINUTES, TimeUnit.MINUTES
);
ALogger.info("Logging has started!");
}
/**
* Shuts down the executor service and saves the chat logs to the database.
* Will throw an error if called on a ChatLogHandler that was started without logging
* Shuts down the executor service and saves the chat logs to the database. Will throw an error if called on a
* ChatLogHandler that was started without logging
*/
public void shutDown() {
executorService.shutdown();
@ -66,7 +77,7 @@ public class ChatLogHandler {
return isSaving;
}
public synchronized void addLog(ChatLog chatLog) {
private synchronized void addLog(ChatLog chatLog) {
if (isBlocked()) {
chatLogQueue.add(chatLog);
} else {
@ -105,12 +116,17 @@ public class ChatLogHandler {
Instant chatTimestamp = resultSet.getTimestamp("time_stamp").toInstant();
String server = resultSet.getString("server");
String chatMessage = resultSet.getString("chat_message");
String stringMiniMessage = resultSet.getString("mini_message");
Component miniMessage = stringMiniMessage == null ? null : GsonComponentSerializer.gson().deserialize(
stringMiniMessage);
boolean chatMessageBlocked = resultSet.getInt("blocked") == 1;
return new ChatLog(chatLogUUID, chatTimestamp, server, chatMessage, chatMessageBlocked);
return new ChatLog(chatLogUUID, chatTimestamp, server, chatMessage, miniMessage, chatMessageBlocked);
}
public void addChatLog(UUID uuid, String server, String message, boolean blocked) {
addLog(new ChatLog(uuid, Instant.now(), server, message, blocked));
public void addChatLog(UUID uuid, String server, String message, Component miniMessage, boolean blocked) {
ChatLog chatLog = new ChatLog(uuid, Instant.now(), server, message, miniMessage, blocked);
addLog(chatLog);
chatLogWebHandler.forwardChatLogToWeb(chatLog);
}
public CompletableFuture<List<ChatLog>> retrieveChatLogs(UUID uuid, Duration duration, String server) {

View File

@ -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<ChatLog> 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<ChatLog> 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<ChatMessageDto> 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();
}
}
}

View File

@ -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

View File

@ -55,7 +55,9 @@ public class ChatListener implements Listener {
}
Component formatComponent = Component.text("%message%");
ComponentLike message = parseMessageContent(event.player(), plainTextComponentSerializer.serialize(event.originalMessage()));
ComponentLike message = parseMessageContent(event.player(),
plainTextComponentSerializer.serialize(event.originalMessage())
);
event.result(formatComponent.replaceText(TextReplacementConfig.builder().match("%message%").replacement(message).build()));
}
@ -67,7 +69,9 @@ public class ChatListener implements Listener {
}
Component formatComponent = Component.text("%message%");
ComponentLike message = parseMessageContent(event.player(), plainTextComponentSerializer.serialize(event.originalMessage()));
ComponentLike message = parseMessageContent(event.player(),
plainTextComponentSerializer.serialize(event.originalMessage())
);
event.result(formatComponent.replaceText(TextReplacementConfig.builder().match("%message%").replacement(message).build()));
}
@ -92,8 +96,9 @@ public class ChatListener implements Listener {
UUID uuid = player.getUniqueId();
ComponentLike input = event.message().colorIfAbsent(NamedTextColor.WHITE);
Component inputComponent = input.asComponent();
ModifiableString modifiableString = new ModifiableString(input.asComponent());
ModifiableString modifiableString = new ModifiableString(inputComponent);
// todo a better way for this
if (!RegexManager.filterText(player.getName(), uuid, modifiableString, true, "chat", filterType -> {
@ -107,12 +112,15 @@ public class ChatListener implements Listener {
out.writeUTF(uuid.toString());
out.writeUTF(modifiableString.string());
player.sendPluginMessage(ChatPlugin.getInstance(), Config.MESSAGECHANNEL, out.toByteArray());
})) {
}
)) {
event.setCancelled(true);
GalaxyUtility.sendBlockedNotification("Language", player,
modifiableString.component(),
"");
chatLogHandler.addChatLog(uuid, ServerName.getServerName(), PlainTextComponentSerializer.plainText().serialize(input.asComponent()), true);
""
);
String originalMessage = PlainTextComponentSerializer.plainText().serialize(inputComponent);
chatLogHandler.addChatLog(uuid, ServerName.getServerName(), originalMessage, inputComponent, true);
return; // the message was blocked
}
@ -120,7 +128,8 @@ public class ChatListener implements Listener {
.map(audience -> (Player) audience);
if (!player.hasPermission("chat.ignorebypass")) {
stream = stream.filter(receiver -> !ChatUserManager.getChatUser(receiver.getUniqueId()).getIgnoredPlayers().contains(uuid)
stream = stream.filter(receiver -> !ChatUserManager.getChatUser(receiver.getUniqueId()).getIgnoredPlayers().contains(
uuid)
|| receiver.hasPermission("chat.ignorebypass"));
}
Set<Player> receivers = stream.collect(Collectors.toSet());
@ -135,8 +144,13 @@ public class ChatListener implements Listener {
for (Player pingPlayer : playersToPing) {
pingPlayer.playSound(pingPlayer.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1);
}
chatLogHandler.addChatLog(uuid, ServerName.getServerName(), modifiableString.string(), false);
ALogger.info(PlainTextComponentSerializer.plainText().serialize(input.asComponent()));
chatLogHandler.addChatLog(uuid,
ServerName.getServerName(),
modifiableString.string(),
modifiableString.component(),
false
);
ALogger.info(PlainTextComponentSerializer.plainText().serialize(inputComponent));
}
private void pingPlayers(Set<Player> playersToPing, ModifiableString modifiableString, Player player) {

View File

@ -35,3 +35,5 @@ pluginManagement {
gradlePluginPortal()
}
}
include("web-api")

View File

@ -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

69
web-api/build.gradle.kts Normal file
View File

@ -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<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("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")
}

View File

@ -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"

View File

@ -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
}