Compare commits

..

No commits in common. "main" and "nicknames_refactor" have entirely different histories.

61 changed files with 486 additions and 2505 deletions

View File

@ -3,20 +3,12 @@ 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
}
compileOnly("org.spongepowered:configurate-yaml:4.2.0") // Configurate
compileOnly("net.luckperms:api:5.5") // Luckperms
//API validation
implementation("org.hibernate.validator:hibernate-validator:9.0.1.Final")
implementation("org.glassfish:jakarta.el:5.0.0-M1")
implementation("com.fasterxml.jackson.core:jackson-databind:2.22.1")
}
publishing {

View File

@ -38,9 +38,9 @@ public final class Config {
CONFIGPATH = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "ChatPlugin");
CONFIG_FILE = new File(CONFIGPATH, "config.yml");
configLoader = YamlConfigurationLoader.builder()
.file(CONFIG_FILE)
.nodeStyle(NodeStyle.BLOCK)
.build();
.file(CONFIG_FILE)
.nodeStyle(NodeStyle.BLOCK)
.build();
if (!CONFIG_FILE.getParentFile().exists()) {
if (!CONFIG_FILE.getParentFile().mkdirs()) {
return;
@ -183,15 +183,11 @@ public final class Config {
private static void settings() {
PREFIXGROUPS = getList("settings.prefix-groups",
Lists.newArrayList("discord", "socialmedia", "eventteam", "eventleader", "youtube", "twitch",
"developer"
)
);
"developer"));
CONFLICTINGPREFIXGROUPS = getList("settings.prefix-conflicts-groups",
Lists.newArrayList("eventteam", "eventleader")
);
Lists.newArrayList("eventteam", "eventleader"));
STAFFGROUPS = getList("settings.staff-groups",
Lists.newArrayList("trainee", "moderator", "headmod", "admin", "manager", "owner")
);
Lists.newArrayList("trainee", "moderator", "headmod", "admin", "manager", "owner"));
CONSOLENAME = getString("settings.console-name", CONSOLENAME);
CONSOLEUUID = UUID.fromString(getString("settings.console-uuid", CONSOLEUUID.toString()));
MINIMIUMSTAFFRANK = getString("settings.minimum-staff-rank", MINIMIUMSTAFFRANK);
@ -286,7 +282,7 @@ public final class Config {
public static String NO_PERMISSION = "<red>You don't have permission to use this command.</red>";
public static String NO_CONSOLE = "<red>This command can not be used by console</red>";
public static String CREATED_PARTY = "<green>You created a chat party called: " +
"'<gold><party_name></gold>' with the password: '<gold><party_password></gold>'</green>";
"'<gold><party_name></gold>' with the password: '<gold><party_password></gold>'</green>";
public static String NOT_IN_A_PARTY = "<red>You're not in a chat party.</red>";
public static String NOT_YOUR_PARTY = "<red>You don't own this chat party.</red>";
public static String NOT_A_PARTY = "<red>This chat party does not exist.</red>";
@ -311,14 +307,14 @@ public final class Config {
public static String ALREADY_IN_THIS_PARTY = "<red>You're already in <party>!</red>";
public static String SENT_PARTY_INV = "<green>You send a chat party invite to <player>!</green>";
public static String JOIN_PARTY_CLICK_MESSAGE = "<click:run_command:'/party join <party> <party_password>'>" +
"<dark_aqua>You received an invite to join <party>, click this message to accept.</dark_aqua></click>";
"<dark_aqua>You received an invite to join <party>, click this message to accept.</dark_aqua></click>";
public static String PARTY_MEMBER_LOGGED_ON = "<dark_aqua>[ChatParty] <player> joined Altitude...</dark_aqua>";
public static String PARTY_MEMBER_LOGGED_OFF = "<dark_aqua>[ChatParty] <player> left Altitude...</dark_aqua>";
public static String RENAMED_PARTY =
"<dark_aqua>[ChatParty] <owner> changed the party name from <old_name> to <new_name>!</dark_aqua>";
public static String CHANGED_PASSWORD = "<green>Password was set to <password></green>";
public static String DISBAND_PARTY_CONFIRM = "<green><bold>Are you sure you want to disband your party?</bold> " +
"Type <gold>/party disband confirm <party></gold> to confirm.";
"Type <gold>/party disband confirm <party></gold> to confirm.";
public static String DISBANDED_PARTY =
"<dark_aqua>[ChatParty] <owner> has disbanded <party>, everyone has been removed.</dark_aqua>";
public static String PARTY_INFO = """
@ -417,8 +413,7 @@ public final class Config {
ConfigurationNode node = getNode("chat-channels");
if (node.empty()) {
getString("chat-channels.ac.format",
"<white><gray><sender></gray> <hover:show_text:on <server>><yellow>to <channel></yellow></hover><gray>: <message>"
);
"<white><gray><sender></gray> <hover:show_text:on <server>><yellow>to <channel></yellow></hover><gray>: <message>");
getList("chat-channels.ac.servers", List.of("lobby"));
getBoolean("chat-channels.ac.proxy", false);
node = getNode("chat-channels");
@ -432,8 +427,7 @@ public final class Config {
getList(key + "servers", Collections.EMPTY_LIST),
getList(key + "alias", Collections.EMPTY_LIST),
getBoolean(key + "proxy", false),
getBoolean(key + "local", false),
getBoolean(key + "web", false)
getBoolean(key + "local", false)
);
}
@ -623,15 +617,11 @@ public final class Config {
NICK_WAIT_TIME = getLong("nicknames.wait-time", NICK_WAIT_TIME);
NICK_ITEM_LORE = getList("nicknames.item-lore",
List.of("<aqua>New nick: <newnick>", "<aqua>Old nick: <oldnick>", "<aqua>Last changed: <lastchanged>",
"<green>Left click to Accept <light_purple>| <red>Right click to Deny"
)
);
"<green>Left click to Accept <light_purple>| <red>Right click to Deny"));
NICK_BLOCKED_COLOR_CODESLIST = getList("nicknames.blocked-color-codes", List.of("&k", "&l", "&n", "&m", "&o"));
NICK_ALLOWED_COLOR_CODESLIST = getList("nicknames.allowed-color-codes",
List.of("&0", "&1", "&2", "&3", "&4", "&5", "&6", "&7", "&8", "&9", "&a", "&b", "&c", "&d", "&e", "&f",
"&r"
)
);
"&r"));
NICK_CURRENT = getString("nicknames.messages.nick-current", NICK_CURRENT);
}
@ -651,14 +641,4 @@ public final class Config {
CHAT_LOG_DELETE_OLDER_THAN_DAYS = getLong("chat-log.delete-older-than-days", CHAT_LOG_DELETE_OLDER_THAN_DAYS);
CHAT_LOG_SAVE_DELAY_MINUTES = getLong("chat-log.save-delay-minutes", CHAT_LOG_SAVE_DELAY_MINUTES);
}
public static String CHAT_WEB_SERVER_BASE_URL = "http://10.0.0.121:8080";
public static String CHAT_WEB_REGISTER_TO_BASE_URL = "https://alttd.com";
public static String CHAT_WEB_TOKEN = "invalid-token";
private static void webServerSettings() {
CHAT_WEB_SERVER_BASE_URL = getString("web-server.base-url", CHAT_WEB_SERVER_BASE_URL);
CHAT_WEB_REGISTER_TO_BASE_URL = getString("web-server.register-to-base-url", CHAT_WEB_REGISTER_TO_BASE_URL);
CHAT_WEB_TOKEN = getString("web-server.token", CHAT_WEB_TOKEN);
}
}

View File

@ -15,25 +15,15 @@ import java.util.concurrent.CompletionException;
public class ChatLogQueries {
protected static void createChatLogTable() {
String nicknamesTableQuery = """
CREATE TABLE IF NOT EXISTS chat_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL,
time_stamp TIMESTAMP(6) NOT NULL,
server VARCHAR(50) NOT NULL,
type VARCHAR(16) NOT NULL DEFAULT 'public',
channel VARCHAR(36) DEFAULT NULL,
receiver CHAR(36) DEFAULT NULL,
chat_message VARCHAR(300) NOT NULL,
mini_message JSON,
blocked BOOLEAN NOT NULL DEFAULT FALSE,
INDEX idx_time_stamp (time_stamp)
)
""";
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"
+ ")";
try (PreparedStatement preparedStatement = DatabaseConnection.getConnection().prepareStatement(
nicknamesTableQuery)) {
try (PreparedStatement preparedStatement = DatabaseConnection.getConnection().prepareStatement(nicknamesTableQuery)) {
preparedStatement.executeUpdate();
} catch (Throwable throwable) {
ALogger.error("Failed to create chat log table", throwable);
@ -41,7 +31,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, type, channel, receiver, chat_message, mini_message, blocked) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
String insertQuery = "INSERT INTO chat_log (uuid, time_stamp, server, chat_message, blocked) VALUES (?, ?, ?, ?, ?)";
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = DatabaseConnection.createTransactionConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
@ -70,7 +60,7 @@ public class ChatLogQueries {
}
public static @NotNull CompletableFuture<List<ChatLog>> retrieveMessages(ChatLogHandler chatLogHandler, UUID uuid, Duration duration, String server) {
String query = "SELECT * FROM chat_log WHERE uuid = ? AND time_stamp > ? AND server = ? AND type = 'public'";
String query = "SELECT * FROM chat_log WHERE uuid = ? AND time_stamp > ? AND server = ?";
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = DatabaseConnection.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(query);
@ -80,14 +70,13 @@ public class ChatLogQueries {
ResultSet resultSet = preparedStatement.executeQuery();
List<ChatLog> chatLogs = new ArrayList<>();
while (resultSet.next()) {
chatLogHandler.loadFromResultSet(resultSet).ifPresent(chatLogs::add);
ChatLog chatLog = chatLogHandler.loadFromResultSet(resultSet);
chatLogs.add(chatLog);
}
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);
}
});
}
@ -101,13 +90,8 @@ 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,5 +1,6 @@
package com.alttd.chat.database;
import com.alttd.chat.config.Config;
import java.sql.Connection;
@ -26,7 +27,6 @@ public class DatabaseConnection {
/**
* Opens the connection if it's not already open.
*
* @throws SQLException If it can't create the connection.
*/
public void openConnection() throws SQLException {
@ -45,16 +45,14 @@ public class DatabaseConnection {
}
connection = DriverManager.getConnection(
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true" +
"&useSSL=false&preserveInstants=true",
Config.USERNAME, Config.PASSWORD
);
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true"+
"&useSSL=false",
Config.USERNAME, Config.PASSWORD);
}
}
/**
* Returns the connection for the database
*
* @return Returns the connection.
*/
public static Connection getConnection() {
@ -71,15 +69,13 @@ public class DatabaseConnection {
* Creates a transactional database connection.
*
* @return A {@code Connection} object representing the transactional database connection.
*
* @throws SQLException If there is an error creating the database connection.
*/
public static Connection createTransactionConnection() throws SQLException {
connection = DriverManager.getConnection(
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true" +
"&useSSL=false&preserveInstants=true",
Config.USERNAME, Config.PASSWORD
);
"jdbc:mysql://" + Config.IP + ":" + Config.PORT + "/" + Config.DATABASE + "?autoReconnect=true"+
"&useSSL=false",
Config.USERNAME, Config.PASSWORD);
connection.setAutoCommit(false);
return connection;
}
@ -92,4 +88,4 @@ public class DatabaseConnection {
return connection != null;
}
}
}

View File

@ -3,10 +3,7 @@ package com.alttd.chat.objects;
import com.alttd.chat.database.Queries;
import com.alttd.chat.objects.channels.Channel;
import com.alttd.chat.util.Utility;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import java.util.ArrayList;
import java.util.List;
@ -92,15 +89,9 @@ public class ChatUser {
return Utility.getStaffPrefix(uuid);
}
public ComponentLike getPrefixAll(boolean isWebMessage) {
public ComponentLike getPrefixAll() {
//return prefixAll;
ComponentLike prefix = Utility.getPrefix(uuid, false);
if (isWebMessage) {
//TODO [Stijn] [2026-08-08]: Check icon
TextComponent webIcon = Component.text("\uD83D\uDEDC").color(NamedTextColor.DARK_AQUA);
prefix = webIcon.append(prefix);
}
return prefix;
return Utility.getPrefix(uuid, false);
}
public String getReplyTarget() {

View File

@ -2,8 +2,6 @@ package com.alttd.chat.objects;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.luckperms.api.model.user.User;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.ArrayList;
@ -20,9 +18,8 @@ public abstract class Toggleable {
public static Toggleable getToggleable(UUID uuid) {
for (Toggleable toggleableClass : togglableClasses) {
if (toggleableClass.isToggled(uuid)) {
if (toggleableClass.isToggled(uuid))
return toggleableClass;
}
}
return null;
}
@ -62,5 +59,4 @@ public abstract class Toggleable {
public abstract void sendMessage(Player player, String message);
public abstract void sendMessage(User user, OfflinePlayer offlinePlayer, String message);
}

View File

@ -1,37 +1,50 @@
package com.alttd.chat.objects.channels;
import lombok.Getter;
import java.util.Collection;
import java.util.HashMap;
@Getter
public class Channel {
public static HashMap<String, Channel> channels = new HashMap<>();
private final String permission;
private final String channelName;
private final String format;
private final boolean proxy;
private final boolean local;
private final boolean web;
private final String webPath;
protected String permission;
protected String channelName;
protected String format;
protected boolean proxy;
protected boolean local;
public Channel(String channelName, String format, boolean proxy, boolean local, boolean web) {
public Channel(String channelName, String format, boolean proxy, boolean local) {
this.permission = "chat.channel." + channelName.toLowerCase();
this.channelName = channelName;
this.format = format;
this.proxy = proxy;
this.local = local;
channels.put(channelName.toLowerCase(), this);
this.web = web;
this.webPath = web ? "web_" + channelName + "_chat" : null;
}
public static Collection<Channel> getChannels() {
return channels.values();
}
public String getPermission() {
return permission;
}
public String getChannelName() {
return channelName;
}
public String getFormat() {
return format;
}
public boolean isProxy() {
return proxy;
}
public boolean isLocal() {
return local;
}
public static Channel getChatChannel(String channelName) {
return channels.get(channelName.toLowerCase());
}

View File

@ -1,19 +1,27 @@
package com.alttd.chat.objects.channels;
import lombok.Getter;
import java.util.List;
@Getter
public class CustomChannel extends Channel {
private final List<String> servers;
private final List<String> aliases;
public CustomChannel(String channelName, String format, List<String> servers, List<String> aliases, boolean proxy,
boolean local, boolean web) {
super(channelName, format, proxy, local, web);
boolean local) {
super(channelName, format, proxy, local);
this.permission = "chat.channel." + channelName.toLowerCase();
this.channelName = channelName;
this.format = format;
this.servers = servers;
this.proxy = proxy;
this.aliases = aliases;
}
public List<String> getServers() {
return servers;
}
public List<String> getAliases() {
return aliases;
}
}

View File

@ -1,7 +1,7 @@
package com.alttd.chat.objects.channels;
public abstract class DefaultChannel extends Channel {
public abstract class DefaultChannel extends Channel{
public DefaultChannel(String channelName, String format, boolean proxy) {
super(channelName, format, proxy, false, false);
super(channelName, format, proxy, false);
}
}

View File

@ -1,12 +1,6 @@
package com.alttd.chat.objects.chat_log;
import com.alttd.chat.objects.BatchInsertable;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogTypeMapper;
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;
@ -15,32 +9,44 @@ import java.sql.Timestamp;
import java.time.Instant;
import java.util.UUID;
@AllArgsConstructor
@Getter
public class ChatLog implements BatchInsertable {
private final UUID uuid;
private final Instant timestamp;
private final String server;
private final ChatLogType type;
private final String channel;
private final String receiver;
private final String message;
private final Component miniMessage;
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, ChatLogTypeMapper.toDb(type));
preparedStatement.setString(5, channel);
preparedStatement.setString(6, receiver);
preparedStatement.setString(7, message);
preparedStatement.setString(8,
miniMessage == null ? null : GsonComponentSerializer.gson().serialize(miniMessage)
);
preparedStatement.setInt(9, blocked ? 1 : 0);
preparedStatement.setString(4, message);
preparedStatement.setInt(5, blocked ? 1 : 0);
}
public UUID getUuid() {
return uuid;
}
public Instant getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public boolean isBlocked() {
return blocked;
}
}

View File

@ -2,12 +2,7 @@ package com.alttd.chat.objects.chat_log;
import com.alttd.chat.config.Config;
import com.alttd.chat.database.ChatLogQueries;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogTypeMapper;
import com.alttd.chat.util.ALogger;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.sql.ResultSet;
@ -17,17 +12,14 @@ import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
public class ChatLogHandler {
private final WebHandler webHandler;
private static ChatLogHandler instance = null;
private ScheduledExecutorService executorService = null;
public static ChatLogHandler getInstance(WebHandler webHandler, boolean enableLogging) {
if (instance == null) {
instance = new ChatLogHandler(webHandler, enableLogging);
}
public static ChatLogHandler getInstance(boolean enableLogging) {
if (instance == null)
instance = new ChatLogHandler(enableLogging);
return instance;
}
@ -35,8 +27,7 @@ public class ChatLogHandler {
private final Queue<ChatLog> chatLogQueue = new ConcurrentLinkedQueue<>();
private final HashMap<UUID, List<ChatLog>> chatLogs = new HashMap<>();
public ChatLogHandler(WebHandler webHandler, boolean enableLogging) {
this.webHandler = webHandler;
public ChatLogHandler(boolean enableLogging) {
if (!enableLogging) {
ALogger.info("Logging is not enabled on this server.");
return;
@ -44,30 +35,23 @@ 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();
@ -82,7 +66,7 @@ public class ChatLogHandler {
return isSaving;
}
private synchronized void addLog(ChatLog chatLog) {
public synchronized void addLog(ChatLog chatLog) {
if (isBlocked()) {
chatLogQueue.add(chatLog);
} else {
@ -116,50 +100,17 @@ public class ChatLogHandler {
});
}
public Optional<ChatLog> loadFromResultSet(@NotNull ResultSet resultSet) throws SQLException {
public ChatLog loadFromResultSet(@NotNull ResultSet resultSet) throws SQLException {
UUID chatLogUUID = UUID.fromString(resultSet.getString("uuid"));
Instant chatTimestamp = resultSet.getTimestamp("time_stamp").toInstant();
String server = resultSet.getString("server");
String stringType = resultSet.getString("type");
ChatLogType type;
try {
type = ChatLogTypeMapper.fromDb(stringType);
} catch (Exception e) {
log.error("Failed to load chat log from result set: {}", e.getMessage());
return Optional.empty();
}
String channel = resultSet.getString("channel");
String receiver = resultSet.getString("receiver");
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 Optional.of(new ChatLog(chatLogUUID,
chatTimestamp,
server,
type,
channel,
receiver,
chatMessage,
miniMessage,
chatMessageBlocked
));
return new ChatLog(chatLogUUID, chatTimestamp, server, chatMessage, chatMessageBlocked);
}
public void addChatLog(UUID uuid, String server, String message, ChatLogType chatLogType, String channel, String receiver, Component miniMessage, boolean blocked) {
ChatLog chatLog = new ChatLog(uuid,
Instant.now(),
server,
chatLogType,
channel,
receiver,
message,
miniMessage,
blocked
);
addLog(chatLog);
webHandler.forwardChatLogToWeb(chatLog);
public void addChatLog(UUID uuid, String server, String message, boolean blocked) {
addLog(new ChatLog(uuid, Instant.now(), server, message, blocked));
}
public CompletableFuture<List<ChatLog>> retrieveChatLogs(UUID uuid, Duration duration, String server) {

View File

@ -1,31 +0,0 @@
package com.alttd.chat.objects.chat_log;
import com.alttd.altitudeweb.invoker.ApiCallback;
import com.alttd.altitudeweb.invoker.ApiException;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
@Slf4j
public class SilentApiHandler implements ApiCallback<Void> {
@Override
public void onFailure(ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {
log.error("Failed to update server states", e);
}
@Override
public void onSuccess(Void result, int statusCode, Map<String, List<String>> responseHeaders) {
}
@Override
public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {
}
@Override
public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {
}
}

View File

@ -1,112 +0,0 @@
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.config.Config;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogMapper;
import com.alttd.chat.objects.chat_log.mapper.server_state.ServerMapper;
import com.alttd.chat.objects.chat_log.mapper.server_state.WebPlayer;
import com.alttd.chat.util.ALogger;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class WebHandler {
private static final int MAX_QUEUE_SIZE = 100;
private final Queue<ChatLog> queue = new ConcurrentLinkedQueue<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final ChatApi chatApi;
public WebHandler() {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(Config.CHAT_WEB_SERVER_BASE_URL);
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();
}
public void updateServerState(String serverName, List<WebPlayer> playerList) {
try {
chatApi.updateServerStatesAsync(ServerMapper.toDto(serverName, playerList), new SilentApiHandler());
} catch (ApiException e) {
log.error("Failed to update server state", e);
}
}
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(ChatLogMapper::toDto)
.filter(Optional::isPresent)
.map(Optional::get)
.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

@ -1,51 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.chat_log;
import com.alttd.altitudeweb.model.ChatMessageDto;
import com.alttd.chat.objects.chat_log.ChatLog;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import java.time.ZoneOffset;
import java.util.Optional;
import java.util.Set;
@Slf4j
@UtilityClass
public class ChatLogMapper {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
public Optional<ChatMessageDto> toDto(ChatLog chatLog) {
ChatMessageDto chatMessageDto = new ChatMessageDto();
if (chatLog.getMiniMessage() == null) {
throw new IllegalArgumentException("MiniMessage cannot be null");
}
chatMessageDto.setUuid(chatLog.getUuid());
chatMessageDto.setTimestamp(chatLog.getTimestamp().atOffset(ZoneOffset.UTC));
chatMessageDto.setServer(chatLog.getServer());
chatMessageDto.setType(ChatLogTypeMapper.toDto(chatLog.getType()));
chatMessageDto.setChannel(chatLog.getChannel());
chatMessageDto.setReceiver(chatLog.getReceiver());
chatMessageDto.setMessage(GsonComponentSerializer.gson().serialize(chatLog.getMiniMessage()));
chatMessageDto.setBlocked(chatLog.isBlocked());
if (checkIsInvalid(chatMessageDto)) {
return Optional.empty();
}
return Optional.of(chatMessageDto);
}
private boolean checkIsInvalid(ChatMessageDto chatMessageDto) {
Set<ConstraintViolation<ChatMessageDto>> violations = validator.validate(chatMessageDto);
if (violations.isEmpty()) {
return false;
}
log.error("Validation failed for chat message: {}, {}", chatMessageDto, violations);
return true;
}
}

View File

@ -1,10 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.chat_log;
public enum ChatLogType {
PUBLIC,
GLOBAL,
PARTY,
GAC,
MSG,
CUSTOM;
}

View File

@ -1,43 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.chat_log;
import com.alttd.altitudeweb.model.ChatMessageDto;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ChatLogTypeMapper {
public static String toDb(ChatLogType chatLogType) {
return switch (chatLogType) {
case PUBLIC -> "public";
case GLOBAL -> "global";
case PARTY -> "party";
case GAC -> "gac";
case MSG -> "msg";
case CUSTOM -> "custom";
};
}
public ChatMessageDto.TypeEnum toDto(ChatLogType chatLogType) {
return switch (chatLogType) {
case PUBLIC -> ChatMessageDto.TypeEnum.PUBLIC;
case GLOBAL -> ChatMessageDto.TypeEnum.GLOBAL;
case PARTY -> ChatMessageDto.TypeEnum.PARTY;
case GAC -> ChatMessageDto.TypeEnum.GAC;
case MSG -> ChatMessageDto.TypeEnum.MSG;
case CUSTOM -> ChatMessageDto.TypeEnum.CUSTOM;
};
}
public ChatLogType fromDb(String dbType) {
return switch (dbType) {
case "public" -> ChatLogType.PUBLIC;
case "global" -> ChatLogType.GLOBAL;
case "party" -> ChatLogType.PARTY;
case "gac" -> ChatLogType.GAC;
case "msg" -> ChatLogType.MSG;
case "custom" -> ChatLogType.CUSTOM;
default -> throw new IllegalArgumentException("Invalid chat log type: " + dbType);
};
}
}

View File

@ -1,22 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.server_state;
import com.alttd.altitudeweb.model.ServerDto;
import com.alttd.altitudeweb.model.ServerStateDto;
import lombok.experimental.UtilityClass;
import java.util.List;
@UtilityClass
public class ServerMapper {
public static ServerStateDto toDto(String serverName, List<WebPlayer> playerList) {
ServerStateDto serverStateDto = new ServerStateDto();
ServerDto serverDto = new ServerDto();
serverDto.setName(serverName);
serverDto.setPlayers(playerList.stream().map(WebPlayerMapper::toDto).toList());
serverStateDto.setServers(List.of(serverDto));
return serverStateDto;
}
}

View File

@ -1,17 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.server_state;
import com.alttd.altitudeweb.model.UserDto;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.bukkit.entity.Player;
@UtilityClass
public class UserMapper {
public static UserDto toDto(Player player) {
UserDto userDto = new UserDto();
userDto.setName(player.getName());
userDto.setUuid(player.getUniqueId());
userDto.setStyledName(GsonComponentSerializer.gson().serialize(player.displayName()));
return userDto;
}
}

View File

@ -1,16 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.server_state;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
public class WebPlayer {
private UUID uuid;
private String name;
private String styledName;
}

View File

@ -1,17 +0,0 @@
package com.alttd.chat.objects.chat_log.mapper.server_state;
import com.alttd.altitudeweb.model.UserDto;
import lombok.experimental.UtilityClass;
@UtilityClass
public class WebPlayerMapper {
public static UserDto toDto(WebPlayer webPlayer) {
UserDto userDto = new UserDto();
userDto.setName(webPlayer.getName());
userDto.setUuid(webPlayer.getUuid());
userDto.setStyledName(webPlayer.getStyledName());
return userDto;
}
}

View File

@ -9,14 +9,11 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.context.ImmutableContextSet;
import net.luckperms.api.model.group.Group;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.Node;
import net.luckperms.api.query.QueryOptions;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -183,27 +180,13 @@ public class Utility {
});
}
public static CompletableFuture<User> getOrLoadUser(UUID uuid) {
return ChatAPI.get().getLuckPerms().getUserManager().loadUser(uuid);
}
public static CompletableFuture<Boolean> hasPermission(UUID uuid, String permission) {
public static boolean hasPermission(UUID uuid, String permission) {
LuckPerms luckPerms = ChatAPI.get().getLuckPerms();
User user = luckPerms.getUserManager().getUser(uuid);
if (user == null) {
return getOrLoadUser(uuid)
.thenApply(loadedUser -> hasPermission(loadedUser, permission))
.exceptionally(throwable -> false);
return false;
}
return CompletableFuture.completedFuture(hasPermission(user, permission));
}
public static boolean hasPermission(User user, String permission) {
LuckPerms luckPerms = ChatAPI.get().getLuckPerms();
return user.getCachedData()
.getPermissionData(QueryOptions.contextual(ImmutableContextSet.of("server", luckPerms.getServerName())))
.checkPermission(permission)
.asBoolean();
return user.getCachedData().getPermissionData().checkPermission(permission).asBoolean();
}
public static ComponentLike applyColor(String message) {

View File

@ -1,194 +0,0 @@
package com.alttd.chat.web;
import com.alttd.chat.config.Config;
import com.alttd.chat.util.ALogger;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Client for subscribing to: GET /api/chat/send/subscribe/{server} served as text/event-stream (SseEmitter) by
* CommandsToServerController.
* <p>
* Handlers for individual SSE event types can be registered up front via {@link #register(String, WebHandler)}. Each
* handler says "when an event named X arrives, deserialize its JSON payload as type T and pass it to this handler."
* Unregistered event names are logged and skipped, not thrown.
* <p>
* If the connection fails or drops for any reason, the client logs it and retries once a minute, indefinitely, until
* stop() is called or the thread is interrupted.
* <p>
* Server-side auth: the controller checks @AuthenticationPrincipal Token token, then token.getKey().equals(validToken).
* That principal is populated from the "Authorization: Bearer <token>" header. validToken is currently hardcoded
* server-side; swap TOKEN below for a config value once that TODO is resolved.
*/
public class SseSubscribeClient implements Runnable {
private static final Logger log = Logger.getLogger(SseSubscribeClient.class.getName());
private static final Duration RETRY_DELAY = Duration.ofMinutes(1);
private final String baseUrl;
private final String server;
private final HttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, WebHandler<?>> handlers = new ConcurrentHashMap<>();
private volatile boolean running = false;
public SseSubscribeClient(String baseUrl, String server, String token) {
this.baseUrl = baseUrl;
this.server = server;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
/**
* Register a handler for a given SSE event name. When an event with this name arrives, its data payload is
* deserialized using the handler's type and passed to the handler.
*/
public <T> void register(String eventName, WebHandler<T> handler) {
handlers.put(eventName, handler);
}
/**
* Runs the subscribe/retry loop on the calling thread. Blocks until stop() is called or the thread is interrupted.
*/
@Override
public void run() {
running = true;
while (running && !Thread.currentThread().isInterrupted()) {
String token = Config.CHAT_WEB_TOKEN;
if (token.equals("invalid-token")) {
log.warning("Invalid token provided for SSE connection as '" + server + "'");
sleepBeforeRetry();
continue;
}
try {
connectAndListen(token);
log.warning("SSE stream as '" + server + "' ended; reconnecting in " + RETRY_DELAY.toMinutes() + " minute(s)");
} catch (Exception e) {
log.log(Level.WARNING, "SSE connection to '" + server + "' failed; retrying in "
+ RETRY_DELAY.toMinutes() + " minute(s)", e
);
}
if (running) {
sleepBeforeRetry();
}
}
}
public void stop() {
running = false;
}
private void connectAndListen(String token) throws Exception {
HttpResponse<InputStream> response = openConnection(token);
checkStatus(response);
readEvents(response.body());
}
private HttpResponse<InputStream> openConnection(String token) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/chat/server/send/subscribe/" + server))
.header("X-Altitude-Token", token)
.header("Accept", "text/event-stream")
.GET()
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
}
private void checkStatus(HttpResponse<InputStream> response) {
int status = response.statusCode();
if (status == 401) {
throw new RuntimeException("Unauthorized - missing/invalid token");
}
if (status == 403) {
throw new RuntimeException("Forbidden - token did not match server's validToken");
}
if (status != 200) {
throw new RuntimeException("Unexpected status: " + status);
}
ALogger.info("SSE connection as '" + server + "' established");
}
/**
* Reads the SSE stream line by line, accumulating one event at a time, and dispatches each complete event as it's
* parsed.
*/
private void readEvents(InputStream body) throws Exception {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8))) {
String eventName = null;
StringBuilder dataBuffer = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
if (!dataBuffer.isEmpty()) {
dispatch(eventName, dataBuffer.toString());
}
eventName = null;
dataBuffer.setLength(0);
continue;
}
if (line.startsWith("event:")) {
eventName = line.substring(6).trim();
} else if (line.startsWith("data:")) {
if (!dataBuffer.isEmpty()) {
dataBuffer.append('\n');
}
dataBuffer.append(line.substring(5).trim());
}
// lines starting with ":" are comments/heartbeats - ignored
}
}
}
private void dispatch(String eventName, String data) {
if (eventName == null) {
log.warning("Received event with no 'event:' name, ignoring. Data: " + data);
return;
}
WebHandler<?> handler = handlers.get(eventName);
if (handler == null) {
log.warning("No handler registered for event type '" + eventName + "', ignoring");
return;
}
dispatchTyped(eventName, handler, data);
}
// Captures the wildcard type from the handler so parsing/casting is type-safe.
private <T> void dispatchTyped(String eventName, WebHandler<T> handler, String data) {
try {
T event = objectMapper.readValue(data, handler.type());
handler.handle(event);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse/handle event '" + eventName + "'", e);
}
}
private void sleepBeforeRetry() {
try {
Thread.sleep(RETRY_DELAY.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
running = false;
}
}
}

View File

@ -1,9 +0,0 @@
package com.alttd.chat.web;
public interface WebHandler<T> {
Class<T> type();
void handle(T event);
}

View File

@ -1,15 +0,0 @@
package com.alttd.chat.web.handler_class;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
public class ChatFromWeb {
private UUID sender;
private String message;
}

View File

@ -1,16 +0,0 @@
package com.alttd.chat.web.handler_class;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
public class PartyChatFromWeb {
private UUID sender;
private String message;
private String partyId;
}

View File

@ -1,16 +0,0 @@
package com.alttd.chat.web.handler_class;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
public class PrivateChatFromWeb {
private UUID sender;
private String message;
private UUID recipient;
}

View File

@ -1,18 +0,0 @@
package com.alttd.chat.web.handler_class;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@NoArgsConstructor
public class PunishFromWeb {
private UUID executor;
private UUID target;
private String type;
private String reason;
private String time;
}

View File

@ -1,6 +1,6 @@
plugins {
`java-library`
id("com.gradleup.shadow") version "9.6.1"
id("io.github.goooler.shadow") version "8.1.8"
id("com.github.ben-manes.versions") version "0.52.0"
}
@ -15,7 +15,7 @@ subprojects {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
languageVersion.set(JavaLanguageVersion.of(21))
}
}

View File

@ -1,6 +1,6 @@
plugins {
`maven-publish`
id("com.gradleup.shadow")
id("io.github.goooler.shadow")
}
val nexusUser = providers.gradleProperty("alttdSnapshotUsername").orNull ?: System.getenv("NEXUS_USERNAME")
@ -8,9 +8,9 @@ val nexusPass = providers.gradleProperty("alttdSnapshotPassword").orNull ?: Syst
dependencies {
implementation(project(":api")) // API
compileOnly("com.alttd.cosmos:cosmos-api:26.2.build.17-stable")
compileOnly("org.projectlombok:lombok:1.18.46")
annotationProcessor("org.projectlombok:lombok:1.18.46")
compileOnly("com.alttd.cosmos:cosmos-api:1.21.8-R0.1-SNAPSHOT") {
isChanging = true
}
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

@ -1,8 +1,5 @@
package com.alttd.chat;
import com.alttd.chat.chat_web.ChatMessageSender;
import com.alttd.chat.chat_web.handlers.WebChannelChatHandler;
import com.alttd.chat.chat_web.handlers.WebChatHandler;
import com.alttd.chat.commands.*;
import com.alttd.chat.config.Config;
import com.alttd.chat.config.ServerConfig;
@ -14,12 +11,9 @@ import com.alttd.chat.nicknames.NicknamesEvents;
import com.alttd.chat.objects.channels.Channel;
import com.alttd.chat.objects.channels.CustomChannel;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.WebHandler;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
import com.alttd.chat.web.SseSubscribeClient;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor;
import org.bukkit.event.Listener;
@ -29,32 +23,23 @@ import java.util.Objects;
public class ChatPlugin extends JavaPlugin {
@Getter
private static ChatPlugin instance;
private ChatAPI chatAPI;
@Getter
private ChatHandler chatHandler;
private ServerConfig serverConfig;
private SseSubscribeClient sseSubscribeClient;
@Override
public void onEnable() {
instance = this;
ALogger.init(getSLF4JLogger());
chatAPI = new ChatImplementation();
WebHandler webHandler = new WebHandler();
ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(webHandler, true);
chatHandler = new ChatHandler(chatLogHandler);
chatHandler = new ChatHandler();
DatabaseConnection.initialize();
serverConfig = new ServerConfig(ServerName.getServerName());
ChatMessageSender chatMessageSender = new ChatMessageSender(chatLogHandler, chatAPI.getLuckPerms());
registerListener(new PlayerListener(webHandler, serverConfig),
new ChatListener(chatMessageSender),
new BookListener(),
new ShutdownListener(chatLogHandler, this)
);
ChatLogHandler chatLogHandler = ChatLogHandler.getInstance(true);
registerListener(new PlayerListener(serverConfig), new ChatListener(chatLogHandler), new BookListener(), new ShutdownListener(chatLogHandler, this));
if (serverConfig.GLOBALCHAT) {
registerCommand("globalchat", new GlobalChat());
registerCommand("toggleglobalchat", new ToggleGlobalChat());
@ -73,41 +58,21 @@ public class ChatPlugin extends JavaPlugin {
if (!(channel instanceof CustomChannel customChannel)) {
continue;
}
ChatChannel chatChannel = new ChatChannel(customChannel, chatAPI.getLuckPerms());
this.getServer()
.getCommandMap()
.register(channel.getChannelName().toLowerCase(), chatChannel);
if (customChannel.getWebPath() != null) {
sseSubscribeClient.register(customChannel.getWebPath(), new WebChannelChatHandler(chatChannel));
}
this.getServer().getCommandMap().register(channel.getChannelName().toLowerCase(), new ChatChannel(customChannel));
}
String messageChannel = Config.MESSAGECHANNEL;
getServer().getMessenger().registerOutgoingPluginChannel(this, messageChannel);
getServer().getMessenger()
.registerIncomingPluginChannel(this, messageChannel, new PluginMessage(chatLogHandler));
getServer().getMessenger().registerIncomingPluginChannel(this, messageChannel, new PluginMessage());
NicknamesEvents nicknamesEvents = new NicknamesEvents();
getServer().getMessenger().registerIncomingPluginChannel(this, messageChannel, nicknamesEvents);
getServer().getPluginManager().registerEvents(nicknamesEvents, this);
registerCommand("nick", new Nicknames());
sseSubscribeClient = new SseSubscribeClient(
Config.CHAT_WEB_REGISTER_TO_BASE_URL,
getServer().getServerName(),
Config.CHAT_WEB_TOKEN
);
new Thread(sseSubscribeClient).start();
registerWebHandlers(chatMessageSender, sseSubscribeClient);
}
private void registerWebHandlers(ChatMessageSender chatMessageSender, SseSubscribeClient sseSubscribeClient) {
sseSubscribeClient.register("web_chat", new WebChatHandler(chatMessageSender));
}
@Override
public void onDisable() {
sseSubscribeClient.stop();
instance = null;
}
@ -121,6 +86,14 @@ public class ChatPlugin extends JavaPlugin {
Objects.requireNonNull(getCommand(commandName)).setExecutor(commandExecutor);
}
public static ChatPlugin getInstance() {
return instance;
}
public ChatHandler getChatHandler() {
return chatHandler;
}
public boolean serverGlobalChatEnabled() {
return serverConfig.GLOBALCHAT;
}
@ -137,9 +110,7 @@ public class ChatPlugin extends JavaPlugin {
chatAPI.reloadConfig();
chatAPI.reloadChatFilters();
serverConfig = new ServerConfig(ServerName.getServerName());
Bukkit.broadcast(Utility.parseMiniMessage("Reloaded ChatPlugin Config.").asComponent(),
"command.chat.reloadchat"
);
Bukkit.broadcast(Utility.parseMiniMessage("Reloaded ChatPlugin Config.").asComponent(), "command.chat.reloadchat");
ALogger.info("Reloaded ChatPlugin config.");
}
}

View File

@ -1,273 +0,0 @@
package com.alttd.chat.chat_web;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.config.Config;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.RegexManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.FilterType;
import com.alttd.chat.objects.ModifiableString;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
public class ChatMessageSender {
private final ChatLogHandler chatLogHandler;
private final LuckPerms luckPerms;
private final MiniMessage miniMessage = MiniMessage.miniMessage();
public void sendMessage(UUID sender, String message) {
//TODO [Stijn] [2026-08-02]: Mark as from website
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(sender);
Component component = miniMessage.deserialize(message);
sendMessage(offlinePlayer, component, true);
}
public void sendMessage(@NotNull OfflinePlayer offlinePlayer, Component message, boolean isFromWeb) {
UUID uuid = offlinePlayer.getUniqueId();
if (luckPerms.getUserManager().isLoaded(uuid)) {
User user = luckPerms.getUserManager().getUser(uuid);
sendMessage(user, offlinePlayer, message, isFromWeb);
} else {
luckPerms.getUserManager().loadUser(uuid).whenComplete((user, throwable) -> {
if (throwable != null) {
ALogger.error("Failed to load user: " + uuid, throwable);
}
sendMessage(user, offlinePlayer, message, isFromWeb);
});
}
}
private void sendMessage(User user, OfflinePlayer offlinePlayer, Component message, boolean isFromWeb) {
if (offlinePlayer == null) {
ALogger.error("OfflinePlayer is null");
return;
}
if (ChatPlugin.getInstance().serverMuted() && !Utility.hasPermission(user, "chat.bypass-server-muted")) {
sendBlockNotifIfOnline(offlinePlayer, message);
return;
}
UUID uuid = offlinePlayer.getUniqueId();
ComponentLike input = message.colorIfAbsent(NamedTextColor.WHITE);
Component inputComponent = input.asComponent();
ModifiableString modifiableString = new ModifiableString(inputComponent);
if (parseMessage(offlinePlayer, uuid, modifiableString, inputComponent, isFromWeb)) {
//Parse message failed likely due to something the player said, already logged
return;
}
if (sendMessageAndPing(user, offlinePlayer, uuid, modifiableString)) {
//Send message and ping failed, likely due to the offlinePlayer not being valid, already logged
return;
}
ALogger.info(PlainTextComponentSerializer.plainText().serialize(inputComponent));
}
private boolean sendMessageAndPing(User user, OfflinePlayer offlinePlayer, UUID uuid, ModifiableString modifiableString) {
ComponentLike input;
Stream<Player> stream = Bukkit.getOnlinePlayers().stream()
.map(audience -> (Player) audience);
if (!Utility.hasPermission(user, "chat.ignorebypass")) {
stream = stream.filter(receiver -> {
boolean isPlayerIgnored = ChatUserManager
.getChatUser(receiver.getUniqueId())
.getIgnoredPlayers()
.contains(uuid);
return !isPlayerIgnored || receiver.hasPermission("chat.ignorebypass");
});
}
Set<Player> receivers = stream.collect(Collectors.toSet());
Set<Player> playersToPing = new HashSet<>();
pingPlayers(playersToPing, modifiableString, offlinePlayer, user);
Optional<ComponentLike> render = render(offlinePlayer, modifiableString.component(), false);
if (render.isEmpty()) {
//Already logged
return true;
}
input = render.get();
for (Player receiver : receivers) {
receiver.sendMessage(input);
}
for (Player pingPlayer : playersToPing) {
pingPlayer.playSound(pingPlayer.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1);
}
chatLogHandler.addChatLog(uuid,
ServerName.getServerName(),
modifiableString.string(),
ChatLogType.PUBLIC,
null,
null,
input.asComponent(),
false
);
return false;
}
private boolean parseMessage(OfflinePlayer offlinePlayer, UUID uuid, ModifiableString modifiableString, Component inputComponent, boolean isWebMessage) {
// todo a better way for this
if (!RegexManager.filterText(offlinePlayer.getName(), uuid, modifiableString, true, "chat",
filterType -> punishOnlinePlayer(filterType, uuid, modifiableString)
)) {
GalaxyUtility.sendBlockedNotification("Language", offlinePlayer,
modifiableString.component(),
""
);
String originalMessage = PlainTextComponentSerializer.plainText().serialize(inputComponent);
Optional<Component> component = render(offlinePlayer,
inputComponent,
isWebMessage
).map(ComponentLike::asComponent);
if (component.isEmpty()) {
//Already logged
return true;
}
chatLogHandler.addChatLog(uuid,
ServerName.getServerName(),
originalMessage,
ChatLogType.PUBLIC,
null,
null,
component.get(),
true
);
return true;
}
return false;
}
private void pingPlayers(Set<Player> playersToPing, ModifiableString modifiableString, OfflinePlayer offlinePlayer, User user) {
Component mention = MiniMessage.miniMessage().deserialize(Config.MENTIONPLAYERTAG);
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
String name = onlinePlayer.getName();
String nickName = PlainTextComponentSerializer.plainText().serialize(onlinePlayer.displayName());
Pattern namePattern = Pattern.compile("\\b(?<!\\\\)" + name + "\\b", Pattern.CASE_INSENSITIVE);
// Pattern escapedNamePattern = Pattern.compile("\\b\\\\" + name + "\\b", Pattern.CASE_INSENSITIVE);
Pattern nickPattern = Pattern.compile("\\b(?<!\\\\)" + nickName + "\\b", Pattern.CASE_INSENSITIVE);
// Pattern escapedNickPattern = Pattern.compile("\\b\\\\" + nickName + "\\b", Pattern.CASE_INSENSITIVE);
ChatUser onlinePlayerUser = ChatUserManager.getChatUser(onlinePlayer.getUniqueId());
if (namePattern.matcher(modifiableString.string()).find()) {
modifiableString.replace(TextReplacementConfig.builder()
.once()
.match(namePattern)
.replacement(mention.append(onlinePlayerUser.getDisplayName()))
.build());
//TODO replace all instances of \name with just name but using the match result so the capitalization doesn't change
// modifiableString.replace(TextReplacementConfig.builder()
// .once()
// .match(escapedNamePattern)
// .replacement((a, b) -> {
// String substring = a.group().substring(1);
// return ;
// });
if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId())
.getIgnoredPlayers()
.contains(offlinePlayer.getUniqueId())
|| Utility.hasPermission(user, "chat.ignorebypass")) {
playersToPing.add(onlinePlayer);
}
} else if (nickPattern.matcher(modifiableString.string()).find()) {
modifiableString.replace(TextReplacementConfig.builder()
.once()
.match(nickPattern)
.replacement(mention.append(onlinePlayerUser.getDisplayName()))
.build());
if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId())
.getIgnoredPlayers()
.contains(offlinePlayer.getUniqueId())
|| Utility.hasPermission(user, "chat.ignorebypass")) {
playersToPing.add(onlinePlayer);
}
}
}
}
private Optional<ComponentLike> render(@NotNull OfflinePlayer offlinePlayer, @NotNull Component message, boolean isWebMessage) {
if (offlinePlayer.getName() == null) {
ALogger.error("Invalid offline player");
return Optional.empty();
}
ChatUser user = ChatUserManager.getChatUser(offlinePlayer.getUniqueId());
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("sender", user.getDisplayName()),
Placeholder.parsed("sendername", offlinePlayer.getName()),
Placeholder.component("prefix", user.getPrefix()),
Placeholder.component("prefixall", user.getPrefixAll(isWebMessage)),
Placeholder.component("staffprefix", user.getStaffPrefix()),
Placeholder.component("message", message)
);
return Optional.of(Utility.parseMiniMessage(Config.CHATFORMAT, placeholders));
}
private static void punishOnlinePlayer(FilterType filterType, UUID uuid, ModifiableString modifiableString) {
Player player = Bukkit.getPlayer(uuid);
if (player == null) {
return;
}
if (!filterType.equals(FilterType.PUNISH)) {
ALogger.warn("Received another FilterType than punish when filtering chat and executing a filter action");
return;
}
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("punish");
out.writeUTF(player.getName());
out.writeUTF(uuid.toString());
out.writeUTF(modifiableString.string());
player.sendPluginMessage(ChatPlugin.getInstance(), Config.MESSAGECHANNEL, out.toByteArray());
}
private static void sendBlockNotifIfOnline(OfflinePlayer offlinePlayer, Component message) {
if (!offlinePlayer.isOnline()) {
return;
}
Player player = offlinePlayer.getPlayer();
if (player == null) {
ALogger.error("Failed to load player: " + offlinePlayer.getName());
return;
}
GalaxyUtility.sendBlockedNotification("Chat Muted", player, message, "");
}
}

View File

@ -1,22 +0,0 @@
package com.alttd.chat.chat_web.handlers;
import com.alttd.chat.commands.ChatChannel;
import com.alttd.chat.web.WebHandler;
import com.alttd.chat.web.handler_class.ChatFromWeb;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class WebChannelChatHandler implements WebHandler<ChatFromWeb> {
private final ChatChannel chatChannel;
@Override
public Class<ChatFromWeb> type() {
return ChatFromWeb.class;
}
@Override
public void handle(ChatFromWeb chatFromWeb) {
this.chatChannel.execute(chatFromWeb);
}
}

View File

@ -1,24 +0,0 @@
package com.alttd.chat.chat_web.handlers;
import com.alttd.chat.chat_web.ChatMessageSender;
import com.alttd.chat.web.WebHandler;
import com.alttd.chat.web.handler_class.ChatFromWeb;
public class WebChatHandler implements WebHandler<ChatFromWeb> {
private final ChatMessageSender chatMessageSender;
public WebChatHandler(ChatMessageSender chatMessageSender) {
this.chatMessageSender = chatMessageSender;
}
@Override
public Class<ChatFromWeb> type() {
return ChatFromWeb.class;
}
@Override
public void handle(ChatFromWeb chatFromWeb) {
this.chatMessageSender.sendMessage(chatFromWeb.getSender(), chatFromWeb.getMessage());
}
}

View File

@ -2,17 +2,10 @@ package com.alttd.chat.commands;
import com.alttd.chat.config.Config;
import com.alttd.chat.objects.channels.CustomChannel;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.ToggleableForCustomChannel;
import com.alttd.chat.util.Utility;
import com.alttd.chat.web.handler_class.ChatFromWeb;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.entity.Player;
@ -20,19 +13,16 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ChatChannel extends BukkitCommand {
CustomChannel channel;
String command;
ToggleableForCustomChannel toggleableForCustomChannel;
private final LuckPerms luckPerms;
private static final List<ChatChannel> activeCommands = new ArrayList<>();
public ChatChannel(CustomChannel channel, LuckPerms luckPerms) {
public ChatChannel(CustomChannel channel) {
super(channel.getChannelName().toLowerCase());
this.luckPerms = luckPerms;
this.channel = channel;
this.command = channel.getChannelName().toLowerCase();
this.description = "Chat channel named " + channel.getChannelName() + ".";
@ -50,12 +40,9 @@ public class ChatChannel extends BukkitCommand {
if (args.length == 0 && player.hasPermission(channel.getPermission())) {
player.sendRichMessage(Config.CUSTOM_CHANNEL_TOGGLED, TagResolver.resolver(
Placeholder.unparsed("channel", channel.getChannelName()),
Placeholder.component("status", toggleableForCustomChannel.toggle(player.getUniqueId())
? Config.TOGGLED_ON : Config.TOGGLED_OFF
)
)
);
Placeholder.unparsed("channel", channel.getChannelName()),
Placeholder.component("status", toggleableForCustomChannel.toggle(player.getUniqueId())
? Config.TOGGLED_ON : Config.TOGGLED_OFF)));
return false;
}
@ -65,28 +52,4 @@ public class ChatChannel extends BukkitCommand {
return false;
}
public void execute(ChatFromWeb chatFromWeb) {
UUID uuid = chatFromWeb.getSender();
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
if (luckPerms.getUserManager().isLoaded(uuid)) {
User user = luckPerms.getUserManager().getUser(uuid);
if (user == null) {
ALogger.error("Failed to load loaded user: " + uuid);
return;
}
if (!Utility.hasPermission(user, channel.getPermission())) {
ALogger.warn("Web user %s does not have permission to use this channel".formatted(uuid.toString()));
return;
}
toggleableForCustomChannel.sendMessage(user, offlinePlayer, chatFromWeb.getMessage());
} else {
luckPerms.getUserManager().loadUser(uuid).whenComplete((user, throwable) -> {
if (throwable != null) {
ALogger.error("Failed to load user: " + uuid, throwable);
}
toggleableForCustomChannel.sendMessage(user, offlinePlayer, chatFromWeb.getMessage());
});
}
}
}

View File

@ -9,8 +9,11 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class MuteServer implements CommandExecutor {
@Override
@ -18,27 +21,27 @@ public class MuteServer implements CommandExecutor {
if (!(sender instanceof Player player)) { // must be a player
return true;
}
Utility.getOrLoadUser(player.getUniqueId()).thenAcceptAsync(user -> {
if (!Utility.hasPermission(user, Config.SERVERMUTEPERMISSION)) {
sender.sendRichMessage("<red>You don't have permission to use this command.</red>");
return;
new BukkitRunnable() {
@Override
public void run() {
UUID uuid = player.getUniqueId();
if (!Utility.hasPermission(uuid, Config.SERVERMUTEPERMISSION)) {
sender.sendRichMessage("<red>You don't have permission to use this command.</red>");
return;
}
ChatPlugin.getInstance().toggleServerMuted();
ComponentLike component;
if (ChatPlugin.getInstance().serverMuted()) {
component = Utility.parseMiniMessage(Utility.getDisplayName(player.getUniqueId(), player.getName()) + " <red>muted</red><white> chat.");
} else {
component = Utility.parseMiniMessage(Utility.getDisplayName(player.getUniqueId(), player.getName()) + " <green>un-muted</green><white> chat.");
}
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(component));
}
ChatPlugin.getInstance().toggleServerMuted();
ComponentLike component;
if (ChatPlugin.getInstance().serverMuted()) {
component = Utility.parseMiniMessage(Utility.getDisplayName(player.getUniqueId(),
player.getName()
) + " <red>muted</red><white> chat.");
} else {
component = Utility.parseMiniMessage(Utility.getDisplayName(player.getUniqueId(),
player.getName()
) + " <green>un-muted</green><white> chat.");
}
Bukkit.getOnlinePlayers().forEach(onlinePlayer -> onlinePlayer.sendMessage(component));
});
}.runTaskAsynchronously(ChatPlugin.getInstance());
return false;
}

View File

@ -5,9 +5,7 @@ import com.alttd.chat.config.Config;
import com.alttd.chat.objects.Toggleable;
import com.alttd.chat.util.ALogger;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.luckperms.api.model.user.User;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -72,9 +70,4 @@ public class PartyChat extends Toggleable implements CommandExecutor {
}
}.runTaskAsynchronously(ChatPlugin.getInstance());
}
@Override
public void sendMessage(User user, OfflinePlayer offlinePlayer, String message) {
//TODO [Stijn] [2026-08-09]: Implement
}
}

View File

@ -1,5 +1,6 @@
package com.alttd.chat.commands;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.config.Config;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.util.Utility;
@ -7,6 +8,7 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@ -15,17 +17,18 @@ public class ToggleGlobalChat implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (!(sender instanceof Player player)) {
if (!(sender instanceof Player)) {
return true;
}
UUID uuid = player.getUniqueId();
Utility.getOrLoadUser(uuid).thenAcceptAsync(user -> {
ChatUserManager.getChatUser(uuid);
Utility.flipPermission(uuid, Config.GCPERMISSION);
sender.sendRichMessage("You have turned globalchat " + (!Utility.hasPermission(user,
Config.GCPERMISSION
) ? "<green>on." : "<red>off.")); // TODO load from config and minimessage
});
new BukkitRunnable() {
@Override
public void run() {
UUID uuid = ((Player) sender).getUniqueId();
ChatUserManager.getChatUser(uuid);
Utility.flipPermission(uuid, Config.GCPERMISSION);
sender.sendRichMessage("You have turned globalchat " + (!Utility.hasPermission(uuid, Config.GCPERMISSION) ? "<green>on." : "<red>off.")); // TODO load from config and minimessage
}
}.runTaskAsynchronously(ChatPlugin.getInstance());
return false;
}

View File

@ -8,15 +8,12 @@ import com.alttd.chat.objects.ChatFilter;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.ModifiableString;
import com.alttd.chat.objects.channels.CustomChannel;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextReplacementConfig;
@ -25,51 +22,39 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.luckperms.api.model.user.User;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Slf4j
public class ChatHandler {
private final ChatPlugin plugin;
private final ComponentLike GCNOTENABLED;
private final ChatLogHandler chatLogHandler;
public ChatHandler(ChatLogHandler chatLogHandler) {
this.chatLogHandler = chatLogHandler;
public ChatHandler() {
plugin = ChatPlugin.getInstance();
GCNOTENABLED = Utility.parseMiniMessage(Config.GCNOTENABLED);
}
public void continuePrivateMessage(Player player, String target, String message) {
Utility.getOrLoadUser(player.getUniqueId())
.thenAccept(user -> continuePrivateMessage(user, player, target, message));
}
public void continuePrivateMessage(User user, Player player, String target, String message) {
// ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
// user.setReplyTarget(target);
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("message", parseMessageContent(user, player, message)),
Placeholder.component("message", parseMessageContent(player, message)),
Placeholder.component("sendername", player.name()),
Placeholder.parsed("receivername", target)
);
);
ComponentLike component = Utility.parseMiniMessage("<message>", placeholders);
@ -77,10 +62,9 @@ public class ChatHandler {
// todo a better way for this
if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "privatemessage")) {
GalaxyUtility.sendBlockedNotification("DM Language",
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
target
);
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
target);
return; // the message was blocked
}
@ -89,37 +73,30 @@ public class ChatHandler {
sendPrivateMessage(player, target, "privatemessage", component.asComponent());
ComponentLike spymessage = Utility.parseMiniMessage(Config.MESSAGESPY, placeholders);
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId())
.isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) {
if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()).isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) {
pl.sendMessage(spymessage);
}
}
}
public void privateMessage(Player player, String target, String message) {
Utility.getOrLoadUser(player.getUniqueId())
.thenAccept(user -> privateMessage(user, player, target, message));
}
public void privateMessage(User user, Player player, String target, String message) {
// ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
// user.setReplyTarget(target);
Component messageComponent = parseMessageContent(user, player, message);
Component messageComponent = parseMessageContent(player, message);
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("message", messageComponent),
Placeholder.component("sendername", player.name()),
Placeholder.parsed("receivername", target)
);
);
ModifiableString modifiableString = new ModifiableString(messageComponent);
// todo a better way for this
if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "privatemessage")) {
GalaxyUtility.sendBlockedNotification("DM Language",
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
target
);
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
target);
return; // the message was blocked
}
@ -131,45 +108,37 @@ public class ChatHandler {
sendPrivateMessage(player, target, "privatemessage", messageComponent);
ComponentLike spymessage = Utility.parseMiniMessage(Config.MESSAGESPY, placeholders);
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId())
.isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) {
if (pl.hasPermission(Config.SPYPERMISSION) && ChatUserManager.getChatUser(pl.getUniqueId()).isSpy() && !pl.equals(player) && !pl.getName().equalsIgnoreCase(target)) {
pl.sendMessage(spymessage);
}
}
}
public void globalChat(Player player, String message) {
Utility.getOrLoadUser(player.getUniqueId())
.thenAccept(user -> globalChat(user, player, message));
}
public void globalChat(User user, Player player, String message) {
ChatUser chatUser = ChatUserManager.getChatUser(player.getUniqueId());
if (!Utility.hasPermission(user, Config.GCPERMISSION)) {
ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
if (!Utility.hasPermission(player.getUniqueId(), Config.GCPERMISSION)) {
player.sendMessage(GCNOTENABLED);// GC IS OFF INFORM THEM ABOUT THIS and cancel
return;
}
if (isMuted(user, player, message, "[GC Muted] ")) {
if (isMuted(player, message, "[GC Muted] ")) {
return;
}
long timeLeft = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - chatUser.getGcCooldown());
long timeLeft = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - user.getGcCooldown());
if (timeLeft <= Config.GCCOOLDOWN && !player.hasPermission("chat.globalchat.cooldownbypass")) { // player is on cooldown and should wait x seconds
player.sendRichMessage(Config.GCONCOOLDOWN,
Placeholder.parsed("cooldown", Config.GCCOOLDOWN - timeLeft + "")
);
player.sendRichMessage(Config.GCONCOOLDOWN, Placeholder.parsed("cooldown", Config.GCCOOLDOWN - timeLeft + ""));
return;
}
ComponentLike senderName = chatUser.getDisplayName();
ComponentLike prefix = chatUser.getPrefix();
ComponentLike senderName = user.getDisplayName();
ComponentLike prefix = user.getPrefix();
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("sender", senderName),
Placeholder.component("prefix", prefix),
Placeholder.component("message", parseMessageContent(user, player, message)),
Placeholder.component("message", parseMessageContent(player, message)),
Placeholder.parsed("server", ServerName.getServerName())
);
);
Component component = Utility.parseMiniMessage(Config.GCFORMAT, placeholders).asComponent();
@ -177,68 +146,45 @@ public class ChatHandler {
// todo a better way for this
if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, "globalchat")) {
GalaxyUtility.sendBlockedNotification("GC Language",
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
""
);
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
"");
return; // the message was blocked
}
component = modifiableString.component();
chatLogHandler.addChatLog(player.getUniqueId(),
Bukkit.getServerName(),
message,
ChatLogType.GLOBAL,
null,
null,
component,
false
);
chatUser.setGcCooldown(System.currentTimeMillis());
user.setGcCooldown(System.currentTimeMillis());
sendPluginMessage(player, "globalchat", component);
}
public void chatChannel(User user, OfflinePlayer offlinePlayer, CustomChannel channel, String message) {
if (!Utility.hasPermission(user, channel.getPermission())) {
if (offlinePlayer.isOnline()) {
Player onlinePlayer = offlinePlayer.getPlayer();
if (onlinePlayer == null) {
log.error("Player is online but getPlayer() returned null");
return;
}
onlinePlayer.sendRichMessage("<red>You don't have permission to use this channel.</red>");
}
public void chatChannel(Player player, CustomChannel channel, String message) {
if (!player.hasPermission(channel.getPermission())) {
player.sendRichMessage("<red>You don't have permission to use this channel.</red>");
return;
}
if (isMuted(user, offlinePlayer, message, "[" + channel.getChannelName() + " Muted] ")) {
if (isMuted(player, message, "[" + channel.getChannelName() + " Muted] ")) {
ALogger.info("Refusing to send message by muted user");
return;
}
ChatUser chatUser = ChatUserManager.getChatUser(offlinePlayer.getUniqueId());
ComponentLike senderName = chatUser.getDisplayName();
ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
ComponentLike senderName = user.getDisplayName();
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("sender", senderName),
Placeholder.component("message", parseMessageContent(user, offlinePlayer, message)),
Placeholder.component("message", parseMessageContent(player, message)),
Placeholder.parsed("server", ServerName.getServerName()),
Placeholder.parsed("channel", channel.getChannelName())
);
);
Component component = Utility.parseMiniMessage(channel.getFormat(), placeholders).asComponent();
ModifiableString modifiableString = new ModifiableString(component);
if (!RegexManager.filterText(offlinePlayer.getName(),
offlinePlayer.getUniqueId(),
modifiableString,
channel.getChannelName()
)) {
if (!RegexManager.filterText(player.getName(), player.getUniqueId(), modifiableString, channel.getChannelName())) {
GalaxyUtility.sendBlockedNotification(channel.getChannelName() + " Language",
offlinePlayer,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
""
);
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
"");
ALogger.info("Refusing to send blocked chat message");
return;
}
@ -246,9 +192,9 @@ public class ChatHandler {
component = modifiableString.component();
if (channel.isProxy()) {
sendChatChannelMessage(offlinePlayer, channel.getChannelName(), "chatchannel", component, message);
sendChatChannelMessage(player, channel.getChannelName(), "chatchannel", component, message);
} else {
sendChatChannelMessage(channel, offlinePlayer.getUniqueId(), component, message);
sendChatChannelMessage(channel, player.getUniqueId(), component, message);
}
}
@ -259,7 +205,7 @@ public class ChatHandler {
out.writeUTF(message);
out.writeUTF(GsonComponentSerializer.gson().serialize(
itemComponent(player.getInventory().getItemInMainHand())
));
));
player.sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray());
// if (isMuted(player, message, "[" + party.getPartyName() + " Muted] ")) return;
@ -309,20 +255,16 @@ public class ChatHandler {
if (!chatChannel.getServers().contains(ServerName.getServerName())) {
player.sendRichMessage("<red>Unable to send messages to <channel> in this server.</red>",
Placeholder.parsed("channel", chatChannel.getChannelName())
);
Placeholder.parsed("channel", chatChannel.getChannelName()));
ALogger.info(String.format("Not sending chat message due to [%s] not being in this channels config",
ServerName.getServerName()
));
ServerName.getServerName()));
return;
}
Stream<? extends Player> stream = Bukkit.getServer().getOnlinePlayers().stream()
.filter(p -> p.hasPermission(chatChannel.getPermission()));
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"));
}
if (chatChannel.isLocal()) {
@ -341,39 +283,14 @@ public class ChatHandler {
List<? extends Player> recipientPlayers = stream.toList();
recipientPlayers.forEach(p -> p.sendMessage(component));
chatLogHandler.addChatLog(player.getUniqueId(),
Bukkit.getServerName(),
message,
ChatLogType.CUSTOM,
chatChannel.getChannelName(),
getLocationIfLocal(chatChannel, player),
component.asComponent(),
false
);
List<UUID> recipientUUIDs = recipientPlayers.stream().map(Entity::getUniqueId).toList();
Bukkit.getServer().getOnlinePlayers().stream()
.filter(onlinePlayer -> onlinePlayer.hasPermission(Config.SPYPERMISSION))
.filter(onlinePlayer -> !recipientUUIDs.contains(onlinePlayer.getUniqueId()))
.forEach(onlinePlayer -> onlinePlayer.sendRichMessage(Config.CHANNEL_SPY,
Placeholder.component("sender", player.name()),
Placeholder.parsed("channel", chatChannel.getChannelName()),
Placeholder.parsed("message", message)
));
}
private static @Nullable String getLocationIfLocal(CustomChannel chatChannel, Player player) {
if (!chatChannel.isLocal()) {
return null;
}
String format = String.format("%s;%s",
player.getLocation().getBlockX(),
player.getLocation().getBlockZ()
);
if (format.length() > 36) {
return null;
}
return format;
Placeholder.component("sender", player.name()),
Placeholder.parsed("channel", chatChannel.getChannelName()),
Placeholder.parsed("message", message)));
}
private void sendPluginMessage(Player player, String channel, Component component) {
@ -393,39 +310,25 @@ public class ChatHandler {
player.sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray());
}
public void sendChatChannelMessage(OfflinePlayer player, String chatChannelName, String channel, Component component, String ignored) {
public void sendChatChannelMessage(Player player, String chatChannelName, String channel, Component component, String ignored) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(channel);
out.writeUTF(chatChannelName);
out.writeUTF(player.getUniqueId().toString());
out.writeUTF(GsonComponentSerializer.gson().serialize(component));
Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers();
if (player.isOnline() && player.getPlayer() != null) {
player.getPlayer().sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray());
} else {
//TODO [Stijn] [2026-08-09]: Validate that this works
onlinePlayers.stream().findFirst().ifPresent(p ->
p.sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray()));
}
player.sendPluginMessage(plugin, Config.MESSAGECHANNEL, out.toByteArray());
}
// Start - move these to util
private boolean isMuted(User user, OfflinePlayer offlinePlayer, String message, String prefix) {
ChatUser chatUser = ChatUserManager.getChatUser(offlinePlayer.getUniqueId());
if (chatUser == null) {
private boolean isMuted(Player player, String message, String prefix) {
ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
if (user == null) {
return false;
}
if (chatUser.isMuted() || (ChatPlugin.getInstance()
.serverMuted() && !Utility.hasPermission(user,
"chat.bypass-server-muted"
))) {
if (user.isMuted() || (ChatPlugin.getInstance().serverMuted() && !player.hasPermission("chat.bypass-server-muted"))) {
// if (Database.get().isPlayerMuted(player.getUniqueId(), null) || (ChatPlugin.getInstance().serverMuted() && !player.hasPermission("chat.bypass-server-muted"))) {
GalaxyUtility.sendBlockedNotification(prefix,
offlinePlayer,
Utility.parseMiniMessage(Utility.stripTokens(message)),
""
);
GalaxyUtility.sendBlockedNotification(prefix, player, Utility.parseMiniMessage(Utility.stripTokens(message)), "");
return true;
}
return false;
@ -473,11 +376,11 @@ public class ChatHandler {
}
// end - move these to util
private Component parseMessageContent(User user, OfflinePlayer offlinePlayer, String rawMessage) {
private Component parseMessageContent(Player player, String rawMessage) {
TagResolver.Builder tagResolver = TagResolver.builder();
Utility.formattingPerms.forEach((perm, pair) -> {
if (Utility.hasPermission(user, perm)) {
if (player.hasPermission(perm)) {
tagResolver.resolver(pair.getX());
}
});
@ -492,16 +395,13 @@ public class ChatHandler {
.replacement(chatFilter.getReplacement()).build());
}
if (offlinePlayer.isOnline() && offlinePlayer.getPlayer() != null) {
component = component
.replaceText(
TextReplacementConfig.builder()
.once()
.matchLiteral("[i]")
.replacement(ChatHandler.itemComponent(offlinePlayer.getPlayer().getInventory()
.getItemInMainHand()))
.build());
}
component = component
.replaceText(
TextReplacementConfig.builder()
.once()
.matchLiteral("[i]")
.replacement(ChatHandler.itemComponent(player.getInventory().getItemInMainHand()))
.build());
return component;

View File

@ -1,32 +1,52 @@
package com.alttd.chat.listeners;
import com.alttd.chat.chat_web.ChatMessageSender;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.config.Config;
import com.alttd.chat.handler.ChatHandler;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.RegexManager;
import com.alttd.chat.objects.ChatFilter;
import com.alttd.chat.objects.Toggleable;
import com.alttd.chat.objects.*;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;
import io.papermc.paper.event.player.AsyncChatDecorateEvent;
import io.papermc.paper.event.player.AsyncChatEvent;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
public class ChatListener implements Listener {
private final PlainTextComponentSerializer plainTextComponentSerializer = PlainTextComponentSerializer.plainText();
private final ChatMessageSender chatMessageSender;
private final ChatLogHandler chatLogHandler;
public ChatListener(ChatLogHandler chatLogHandler) {
this.chatLogHandler = chatLogHandler;
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onChatCommandDecorate(AsyncChatCommandDecorateEvent event) {
@ -35,14 +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()));
event.result(formatComponent.replaceText(TextReplacementConfig.builder().match("%message%").replacement(message).build()));
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@ -52,16 +67,13 @@ 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()));
event.result(formatComponent.replaceText(TextReplacementConfig.builder().match("%message%").replacement(message).build()));
}
private final Component mention = MiniMessage.miniMessage().deserialize(Config.MENTIONPLAYERTAG);
@EventHandler(ignoreCancelled = true)
public void onPlayerChat(AsyncChatEvent event) {
event.setCancelled(true); //Always cancel the event because we do not want to deal with Microsoft's stupid bans
@ -70,7 +82,119 @@ public class ChatListener implements Listener {
toggleable.sendMessage(event.getPlayer(), event.message());
return;
}
chatMessageSender.sendMessage(event.getPlayer(), event.message(), false);
if (ChatPlugin.getInstance().serverMuted() && !event.getPlayer().hasPermission("chat.bypass-server-muted")) {
Player player = event.getPlayer();
GalaxyUtility.sendBlockedNotification("Chat Muted", player, event.message(), "");
return;
}
Player player = event.getPlayer();
UUID uuid = player.getUniqueId();
ComponentLike input = event.message().colorIfAbsent(NamedTextColor.WHITE);
ModifiableString modifiableString = new ModifiableString(input.asComponent());
// todo a better way for this
if (!RegexManager.filterText(player.getName(), uuid, modifiableString, true, "chat", filterType -> {
if (!filterType.equals(FilterType.PUNISH)) {
ALogger.warn("Received another FilterType than punish when filtering chat and executing a filter action");
return;
}
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("punish");
out.writeUTF(player.getName());
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);
return; // the message was blocked
}
Stream<Player> stream = event.viewers().stream().filter(audience -> audience instanceof Player)
.map(audience -> (Player) audience);
if (!player.hasPermission("chat.ignorebypass")) {
stream = stream.filter(receiver -> !ChatUserManager.getChatUser(receiver.getUniqueId()).getIgnoredPlayers().contains(uuid)
|| receiver.hasPermission("chat.ignorebypass"));
}
Set<Player> receivers = stream.collect(Collectors.toSet());
Set<Player> playersToPing = new HashSet<>();
pingPlayers(playersToPing, modifiableString, player);
input = render(player, modifiableString.component());
for (Player receiver : receivers) {
receiver.sendMessage(input);
}
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()));
}
private void pingPlayers(Set<Player> playersToPing, ModifiableString modifiableString, Player player) {
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
String name = onlinePlayer.getName();
String nickName = PlainTextComponentSerializer.plainText().serialize(onlinePlayer.displayName());
Pattern namePattern = Pattern.compile("\\b(?<!\\\\)" + name + "\\b", Pattern.CASE_INSENSITIVE);
// Pattern escapedNamePattern = Pattern.compile("\\b\\\\" + name + "\\b", Pattern.CASE_INSENSITIVE);
Pattern nickPattern = Pattern.compile("\\b(?<!\\\\)" + nickName + "\\b", Pattern.CASE_INSENSITIVE);
// Pattern escapedNickPattern = Pattern.compile("\\b\\\\" + nickName + "\\b", Pattern.CASE_INSENSITIVE);
ChatUser onlinePlayerUser = ChatUserManager.getChatUser(onlinePlayer.getUniqueId());
if (namePattern.matcher(modifiableString.string()).find()) {
modifiableString.replace(TextReplacementConfig.builder()
.once()
.match(namePattern)
.replacement(mention.append(onlinePlayerUser.getDisplayName()))
.build());
//TODO replace all instances of \name with just name but using the match result so the capitalization doesn't change
// modifiableString.replace(TextReplacementConfig.builder()
// .once()
// .match(escapedNamePattern)
// .replacement((a, b) -> {
// String substring = a.group().substring(1);
// return ;
// });
if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()).getIgnoredPlayers().contains(player.getUniqueId())
|| player.hasPermission("chat.ignorebypass")) {
playersToPing.add(onlinePlayer);
}
} else if (nickPattern.matcher(modifiableString.string()).find()) {
modifiableString.replace(TextReplacementConfig.builder()
.once()
.match(nickPattern)
.replacement(mention.append(onlinePlayerUser.getDisplayName()))
.build());
if (!ChatUserManager.getChatUser(onlinePlayer.getUniqueId()).getIgnoredPlayers().contains(player.getUniqueId())
|| player.hasPermission("chat.ignorebypass")) {
playersToPing.add(onlinePlayer);
}
}
}
}
public @NotNull ComponentLike render(@NotNull Player player, @NotNull Component message) {
ChatUser user = ChatUserManager.getChatUser(player.getUniqueId());
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("sender", user.getDisplayName()),
Placeholder.parsed("sendername", player.getName()),
Placeholder.component("prefix", user.getPrefix()),
Placeholder.component("prefixall", user.getPrefixAll()),
Placeholder.component("staffprefix", user.getStaffPrefix()),
Placeholder.component("message", message)
);
return Utility.parseMiniMessage(Config.CHATFORMAT, placeholders);
}
private ComponentLike parseMessageContent(Player player, String rawMessage) {

View File

@ -8,7 +8,6 @@ import com.alttd.chat.managers.RegexManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.ModifiableString;
import com.alttd.chat.objects.Toggleable;
import com.alttd.chat.objects.chat_log.WebHandler;
import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.Utility;
import net.kyori.adventure.text.Component;
@ -31,7 +30,6 @@ import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;
import java.util.UUID;
@ -40,10 +38,8 @@ import java.util.concurrent.TimeUnit;
public class PlayerListener implements Listener {
private final ServerConfig serverConfig;
private final WebHandler webHandler;
public PlayerListener(WebHandler webHandler, ServerConfig serverConfig) {
this.webHandler = webHandler;
public PlayerListener(ServerConfig serverConfig) {
this.serverConfig = serverConfig;
}
@ -54,23 +50,19 @@ public class PlayerListener implements Listener {
UUID uuid = player.getUniqueId();
Toggleable.disableToggles(uuid);
if (serverConfig.FIRST_JOIN_MESSAGES && (!player.hasPlayedBefore() || System.currentTimeMillis() - player.getFirstPlayed() < TimeUnit.SECONDS.toMillis(
10))) {
Bukkit.broadcast(MiniMessage.miniMessage()
.deserialize(Config.FIRST_JOIN, Placeholder.parsed("player", player.getName())));
if (serverConfig.FIRST_JOIN_MESSAGES && (!player.hasPlayedBefore() || System.currentTimeMillis() - player.getFirstPlayed() < TimeUnit.SECONDS.toMillis(10))) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Config.FIRST_JOIN, Placeholder.parsed("player", player.getName())));
}
ChatUser user = ChatUserManager.getChatUser(uuid);
if (user != null) {
updateServerState(event.getPlayer(), user, true);
return;
}
if(user != null) return;
// user failed to load - create a new one
ChatUser chatUser = new ChatUser(uuid, -1, null);
ChatUserManager.addUser(chatUser);
Queries.saveUser(chatUser);
updateServerState(event.getPlayer(), chatUser, true);
//TODO load player on other servers with plugin message?
}
@ -79,26 +71,8 @@ public class PlayerListener implements Listener {
UUID uuid = event.getPlayer().getUniqueId();
ChatUser user = ChatUserManager.getChatUser(uuid);
ChatUserManager.removeUser(user);
updateServerState(event.getPlayer(), user, false);
}
private void updateServerState(Player triggerPlayer, ChatUser chatUser, boolean add) {
ArrayList<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers());
if (add) {
if (playerList.stream()
.filter(player -> player.getUniqueId().equals(triggerPlayer.getUniqueId()))
.findFirst()
.isEmpty()) {
playerList.add(triggerPlayer);
}
} else {
playerList.removeIf(player -> player.getUniqueId().equals(triggerPlayer.getUniqueId()));
}
String name = Bukkit.getServer().getName();
webHandler.updateServerState(name,
playerList.stream().map(player -> WebPlayerMapper.fromPlayer(player, chatUser)).toList()
);
}
@EventHandler(ignoreCancelled = true) // untested
public void onSignChangeE(SignChangeEvent event) {
@ -114,8 +88,7 @@ public class PlayerListener implements Listener {
GalaxyUtility.sendBlockedNotification("Sign Language",
player,
Utility.parseMiniMessage(Utility.parseColors(modifiableString.string())),
""
);
"");
}
component = modifiableString.component() == null ? Component.empty() : modifiableString.component();
@ -126,7 +99,6 @@ public class PlayerListener implements Listener {
}
private final HashMap<UUID, Stack<Instant>> sendPlayerDeaths = new HashMap<>();
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onPlayerDeath(@NotNull PlayerDeathEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
@ -141,7 +113,7 @@ public class PlayerListener implements Listener {
event.deathMessage(Component.empty());
return;
}
Component component = event.deathMessage();
Component component = event.deathMessage();
playerDeathsStack.push(Instant.now());
if (component == null) {
@ -160,9 +132,7 @@ public class PlayerListener implements Listener {
.build();
component = component.replaceText(killerReplacement);
}
component = MiniMessage.miniMessage()
.deserialize("<dark_red>[</dark_red><red>☠</red><dark_red>]</dark_red> ")
.append(component);
component = MiniMessage.miniMessage().deserialize("<dark_red>[</dark_red><red>☠</red><dark_red>]</dark_red> ").append(component);
component = component.style(Style.style(TextColor.color(255, 155, 48), TextDecoration.ITALIC));
event.deathMessage(component);
}

View File

@ -10,8 +10,6 @@ import com.alttd.chat.objects.Party;
import com.alttd.chat.objects.PartyUser;
import com.alttd.chat.objects.channels.Channel;
import com.alttd.chat.objects.channels.CustomChannel;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
@ -20,7 +18,6 @@ import com.google.common.io.ByteStreams;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
@ -32,12 +29,6 @@ import java.util.UUID;
public class PluginMessage implements PluginMessageListener {
private final ChatLogHandler chatLogHandler;
public PluginMessage(ChatLogHandler chatLogHandler) {
this.chatLogHandler = chatLogHandler;
}
@Override
public void onPluginMessageReceived(String channel, @NotNull Player ignored, byte[] bytes) {
if (!channel.equals(Config.MESSAGECHANNEL)) {
@ -57,25 +48,13 @@ public class PluginMessage implements PluginMessageListener {
}
ChatUser chatUser = ChatUserManager.getChatUser(uuid);
if (isTargetNotIgnored(chatUser, targetuuid)) {
Component component = GsonComponentSerializer.gson().deserialize(message);
player.sendMessage(component);
player.sendMessage(GsonComponentSerializer.gson().deserialize(message));
player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1,
1
); // todo load this from config
1); // todo load this from config
ChatUser user = ChatUserManager.getChatUser(uuid);
if (!user.getReplyContinueTarget().equalsIgnoreCase(target)) {
user.setReplyTarget(target);
}
// Handled here since this is after the ignore check and is only send once
chatLogHandler.addChatLog(uuid,
ServerName.getServerName(),
PlainTextComponentSerializer.plainText().serialize(component),
ChatLogType.MSG,
null,
player.getUniqueId().toString(),
component,
false
);
}
}
case "privatemessageout" -> {
@ -90,18 +69,9 @@ public class PluginMessage implements PluginMessageListener {
ChatUser chatUser = ChatUserManager.getChatUser(uuid);
if (isTargetNotIgnored(chatUser, targetuuid)) {
chatUser.setReplyTarget(target);
Component component = GsonComponentSerializer.gson().deserialize(message);
player.sendMessage(component);
// Handled here since this is after the ignore check and is only send once
chatLogHandler.addChatLog(uuid,
ServerName.getServerName(),
PlainTextComponentSerializer.plainText().serialize(component),
ChatLogType.MSG,
null,
player.getUniqueId().toString(),
component,
false
);
player.sendMessage(GsonComponentSerializer.gson().deserialize(message));
// ChatUser user = ChatUserManager.getChatUser(uuid);
// user.setReplyTarget(target);
}
}
case "globalchat" -> {
@ -112,11 +82,10 @@ public class PluginMessage implements PluginMessageListener {
UUID uuid = UUID.fromString(in.readUTF());
String message = in.readUTF();
Component component = GsonComponentSerializer.gson().deserialize(message);
Bukkit.getOnlinePlayers().stream().filter(p -> p.hasPermission(Config.GCPERMISSION)).forEach(p -> {
ChatUser chatUser = ChatUserManager.getChatUser(p.getUniqueId());
if (isTargetNotIgnored(chatUser, uuid)) {
p.sendMessage(component);
p.sendMessage(GsonComponentSerializer.gson().deserialize(message));
}
});
}
@ -138,7 +107,6 @@ public class PluginMessage implements PluginMessageListener {
}
chatChannel(in);
//TODO [Stijn] [2026-07-19]: handle custom channels
}
case "tmppartyupdate" -> {
int id = Integer.parseInt(in.readUTF());

View File

@ -1,19 +0,0 @@
package com.alttd.chat.listeners;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.chat_log.mapper.server_state.WebPlayer;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.bukkit.entity.Player;
@UtilityClass
public class WebPlayerMapper {
public static WebPlayer fromPlayer(Player player, ChatUser chatUser) {
WebPlayer webPlayer = new WebPlayer();
webPlayer.setUuid(player.getUniqueId());
webPlayer.setName(player.getName());
webPlayer.setStyledName(GsonComponentSerializer.gson().serialize(chatUser.getDisplayName().asComponent()));
return webPlayer;
}
}

View File

@ -119,7 +119,8 @@ public class NicknamesEvents implements Listener, PluginMessageListener {
switch (subChannel) {
case "NickNameRequest":
ComponentLike component = miniMessage.deserialize(Config.NICK_REQUEST_NEW, Placeholder.parsed("player", name))
.clickEvent(ClickEvent.runCommand("/nick review"))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND,
"/nick review"))
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT,
miniMessage.deserialize("<gold>Click this text to review the request!")));

View File

@ -6,7 +6,6 @@ import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.ArrayList;
@ -14,14 +13,13 @@ import java.util.List;
public class GalaxyUtility {
public static void sendBlockedNotification(String prefix, OfflinePlayer offlinePlayer, ComponentLike input, String target) {
String playerName = offlinePlayer.getName() == null ? "unknown_player" : offlinePlayer.getName();
public static void sendBlockedNotification(String prefix, Player player, ComponentLike input, String target) {
TagResolver placeholders = TagResolver.resolver(
Placeholder.parsed("prefix", prefix),
Placeholder.parsed("displayname", Utility.getDisplayName(offlinePlayer.getUniqueId(), playerName)),
Placeholder.parsed("displayname", Utility.getDisplayName(player.getUniqueId(), player.getName())),
Placeholder.parsed("target", (target.isEmpty() ? "tried to say:" : "-> " + target + ":")),
Placeholder.component("input", input)
);
);
ComponentLike blockedNotification = Utility.parseMiniMessage(Config.NOTIFICATIONFORMAT, placeholders);
Bukkit.getOnlinePlayers().forEach(a -> {
@ -29,15 +27,8 @@ public class GalaxyUtility {
a.sendMessage(blockedNotification);
}
});
if (offlinePlayer.isOnline()) {
Player player = offlinePlayer.getPlayer();
if (player == null) {
ALogger.error("Player is offline but isOnline() returned true");
return;
}
player.sendRichMessage("<red>The language you used in your message is not allowed, " +
"this constitutes as your only warning. Any further attempts at bypassing the filter will result in staff intervention.</red>");
}
player.sendRichMessage("<red>The language you used in your message is not allowed, " +
"this constitutes as your only warning. Any further attempts at bypassing the filter will result in staff intervention.</red>");
}
public static void addAdditionalChatCompletions(Player player) {

View File

@ -3,8 +3,6 @@ package com.alttd.chat.util;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.objects.Toggleable;
import com.alttd.chat.objects.channels.CustomChannel;
import net.luckperms.api.model.user.User;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
@ -40,27 +38,11 @@ public class ToggleableForCustomChannel extends Toggleable {
@Override
public void sendMessage(Player player, String message) {
Utility.getOrLoadUser(player.getUniqueId()).thenAcceptAsync(user -> {
ALogger.info(String.format("%s sent %s message: %s",
player.getName(),
customChannel.getChannelName(),
message
));
ChatPlugin.getInstance().getChatHandler().chatChannel(user, player, customChannel, message);
});
}
@Override
public void sendMessage(User user, OfflinePlayer offlinePlayer, String message) {
new BukkitRunnable() {
@Override
public void run() {
ALogger.info(String.format("%s sent %s message: %s",
offlinePlayer.getName(),
customChannel.getChannelName(),
message
));
ChatPlugin.getInstance().getChatHandler().chatChannel(user, offlinePlayer, customChannel, message);
ALogger.info(String.format("%s sent %s message: %s", player.getName(), customChannel.getChannelName(), message));
ChatPlugin.getInstance().getChatHandler().chatChannel(player, customChannel, message);
}
}.runTaskAsynchronously(ChatPlugin.getInstance());
}

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

15
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,6 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -171,6 +170,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -203,14 +203,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.

25
gradlew.bat vendored
View File

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@ -59,21 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@ -19,19 +19,13 @@ dependencyResolutionManagement {
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") // Papi
maven("https://jitpack.io")
maven {
name = "nexus"
url = uri("https://repo.alttd.com/repository/alttd-snapshot/")
credentials {
username = nexusUser
password = nexusPass
}
}
maven {
url = uri("https://repo.alttd.com/repository/alttd/")
credentials {
username = nexusUser
password = nexusPass
}
}
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
}
@ -41,5 +35,3 @@ pluginManagement {
gradlePluginPortal()
}
}
include("web-api")

View File

@ -1,12 +1,10 @@
plugins {
`maven-publish`
id("com.gradleup.shadow")
id("io.github.goooler.shadow")
}
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
@ -14,19 +12,10 @@ dependencies {
compileOnly("net.kyori:adventure-text-minimessage:4.23.0")
compileOnly("com.gitlab.ruany:LiteBansAPI:0.3.5")
compileOnly("com.alttd.proxydiscordlink:ProxyDiscordLink:1.0.1-SNAPSHOT")
compileOnly("net.luckperms:api:5.5") // Luckperms
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks {
test {
useJUnitPlatform()
}
shadowJar {
archiveFileName.set("${rootProject.name}-${project.name}-${project.version}.jar")
// minimize()

View File

@ -2,24 +2,20 @@ package com.alttd.velocitychat;
import com.alttd.chat.ChatAPI;
import com.alttd.chat.ChatImplementation;
import com.alttd.chat.config.Config;
import com.alttd.chat.database.DatabaseConnection;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.PartyManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.WebHandler;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.web.SseSubscribeClient;
import com.alttd.velocitychat.chat_web.handlers.PunishFromWebHandler;
import com.alttd.velocitychat.chat_web.handlers.WebPartyChatHandler;
import com.alttd.velocitychat.commands.*;
import com.alttd.chat.config.Config;
import com.alttd.chat.database.DatabaseConnection;
import com.alttd.velocitychat.handlers.ChatHandler;
import com.alttd.velocitychat.handlers.ServerHandler;
import com.alttd.velocitychat.listeners.ChatListener;
import com.alttd.velocitychat.listeners.LiteBansListener;
import com.alttd.velocitychat.listeners.PluginMessageListener;
import com.alttd.velocitychat.listeners.ProxyPlayerListener;
import com.alttd.velocitychat.listeners.PluginMessageListener;
import com.alttd.chat.util.ALogger;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
@ -40,7 +36,7 @@ import java.nio.file.Path;
description = "A chat plugin for Altitude Minecraft Server",
authors = {"destro174", "teri"},
dependencies = {@Dependency(id = "luckperms"), @Dependency(id = "litebans"), @Dependency(id = "proxydiscordlink")}
)
)
public class VelocityChat {
private static VelocityChat plugin;
@ -53,7 +49,6 @@ public class VelocityChat {
private ServerHandler serverHandler;
private ChannelIdentifier channelIdentifier;
private SseSubscribeClient sseSubscribeClient;
@Inject
public VelocityChat(ProxyServer proxyServer, Logger proxyLogger, @DataDirectory Path proxydataDirectory) {
@ -72,10 +67,7 @@ public class VelocityChat {
PartyManager.initialize(); // load the parties from the db and add the previously loaded users to them
serverHandler = new ServerHandler();
WebHandler webHandler = new WebHandler();
ChatLogHandler chatLogHandler = new ChatLogHandler(webHandler, true);
chatHandler = new ChatHandler(chatLogHandler, chatAPI.getLuckPerms());
chatHandler = new ChatHandler();
server.getEventManager().register(this, new ChatListener());
server.getEventManager().register(this, new ProxyPlayerListener());
new LiteBansListener().init(); // init the litebans api listeners
@ -83,23 +75,11 @@ public class VelocityChat {
channelIdentifier = MinecraftChannelIdentifier.create(channels[0], channels[1]);
server.getChannelRegistrar().register(channelIdentifier);
server.getEventManager().register(this, new PluginMessageListener(channelIdentifier));
loadCommands(webHandler);
loadCommands();
// setup console chatuser
ChatUser console = new ChatUser(Config.CONSOLEUUID, -1, null);
console.setDisplayName(Config.CONSOLENAME);
ChatUserManager.addUser(console);
sseSubscribeClient = new SseSubscribeClient(
Config.CHAT_WEB_REGISTER_TO_BASE_URL,
"proxy", //TODO [Stijn] [2026-08-09]: Make configurable if needed
Config.CHAT_WEB_TOKEN
);
new Thread(sseSubscribeClient).start();
registerWebHandlers(sseSubscribeClient);
}
private void registerWebHandlers(SseSubscribeClient sseSubscribeClient) {
sseSubscribeClient.register("web_party_chat", new WebPartyChatHandler(chatHandler));
sseSubscribeClient.register("web_punish", new PunishFromWebHandler(server));
}
public void reloadConfig() {
@ -109,11 +89,7 @@ public class VelocityChat {
ByteArrayDataOutput buf = ByteStreams.newDataOutput();
buf.writeUTF("reloadconfig");
ALogger.info("Reloaded ChatPlugin proxy config.");
getProxy().getAllServers()
.stream()
.forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(),
buf.toByteArray()
));
getProxy().getAllServers().stream().forEach(registeredServer -> registeredServer.sendPluginMessage(getChannelIdentifier(), buf.toByteArray()));
}
public File getDataDirectory() {
@ -132,8 +108,8 @@ public class VelocityChat {
return server;
}
public void loadCommands(WebHandler webHandler) {
ChatLogHandler instance = ChatLogHandler.getInstance(webHandler, false);
public void loadCommands() {
ChatLogHandler instance = ChatLogHandler.getInstance(false);
new SilentJoinCommand(server);
new GlobalAdminChat(server);
new Reload(server);

View File

@ -1,120 +0,0 @@
package com.alttd.velocitychat.chat_web;
import com.alttd.chat.web.handler_class.PunishFromWeb;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
public class PunishmentCommandBuilder {
private static final Set<String> ALLOWED_TYPES = Set.of("BAN", "MUTE", "WARN");
private static final String WARN_DURATION = "30d";
public static String buildCommand(String executorName, PunishFromWeb event) {
String rawType = event.getType();
UUID executorUuid = event.getExecutor();
String target = event.getTarget().toString();
String reason = event.getReason();
String type = validateType(rawType);
validateReason(reason);
String time = resolveTime(type, event.getTime());
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append(type).append(" ").append(target);
if (time != null) {
commandBuilder.append(" ").append(time);
}
commandBuilder.append(" --sender=").append(executorName)
.append(" --sender-uuid=").append(executorUuid);
commandBuilder.append(" ").append(reason);
return commandBuilder.toString();
}
private static String validateType(String rawType) {
if (rawType == null || !ALLOWED_TYPES.contains(rawType.toUpperCase(Locale.ROOT))) {
throw new IllegalArgumentException("Invalid punishment type: " + rawType
+ ". Allowed types are: " + ALLOWED_TYPES);
}
return rawType.toLowerCase(Locale.ROOT);
}
private static void validateReason(String reason) {
if (reason == null || reason.isBlank()) {
throw new IllegalArgumentException("A reason is required for all punishments");
}
}
/**
* Resolves the litebans-formatted time argument for a given (already-normalized,
* lowercase) punishment type, applying the per-type rules:
* - warn: always 30d, regardless of what was supplied
* - mute: a duration is required
* - ban: optional, permanent (null) if not supplied
*/
private static String resolveTime(String type, String rawTime) {
switch (type) {
case "warn":
return WARN_DURATION;
case "mute":
if (rawTime == null || rawTime.isBlank()) {
throw new IllegalArgumentException("Mutes must have a duration");
}
return parseDuration(rawTime);
case "ban":
if (rawTime == null || rawTime.isBlank()) {
return null; // permanent ban
}
return parseDuration(rawTime);
default:
// unreachable, type is already validated before this is called
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
/**
* Parses an ISO-8601 duration (e.g. "PT30M", "P7D", "P1DT2H3M4S") as sent by the
* OpenAPI spec's `time` field, and converts it into a litebans-style duration
* string. Litebans only accepts a single unit of d, h, or m (no combined units,
* no seconds), so this picks the single largest whole unit and drops the rest,
* e.g. "P1DT2H3M4S" -> "1d", "PT2H3M" -> "2h", "PT45M" -> "45m".
*/
static String parseDuration(String isoDuration) {
Duration duration;
try {
duration = Duration.parse(isoDuration);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid duration format: " + isoDuration, e);
}
long totalSeconds = duration.getSeconds();
if (totalSeconds <= 0) {
throw new IllegalArgumentException("Duration must be positive: " + isoDuration);
}
long days = totalSeconds / 86400;
if (days > 0) {
return days + "d";
}
long hours = totalSeconds / 3600;
if (hours > 0) {
return hours + "h";
}
long minutes = totalSeconds / 60;
if (minutes > 0) {
return minutes + "m";
}
//Default minimum
return "1m";
}
}

View File

@ -1,42 +0,0 @@
package com.alttd.velocitychat.chat_web.handlers;
import com.alttd.chat.util.Utility;
import com.alttd.chat.web.WebHandler;
import com.alttd.chat.web.handler_class.PunishFromWeb;
import com.alttd.velocitychat.chat_web.PunishmentCommandBuilder;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PunishFromWebHandler implements WebHandler<PunishFromWeb> {
private final ProxyServer server;
public PunishFromWebHandler(ProxyServer server) {
this.server = server;
}
@Override
public Class<PunishFromWeb> type() {
return PunishFromWeb.class;
}
@Override
public void handle(PunishFromWeb event) {
String permission = "litebans." + event.getType().toLowerCase();
Utility.getOrLoadUser(event.getExecutor()).thenAccept(user -> {
if (user == null) {
log.warn("User {} does not exist", event.getExecutor());
return;
}
if (!Utility.hasPermission(user, permission)) {
log.warn("User {} does not have permission {}", user.getUsername(), permission);
return;
}
String executorName = (user.getUsername() != null) ? user.getUsername() : event.getExecutor().toString();
server.getCommandManager()
.executeAsync(server.getConsoleCommandSource(),
PunishmentCommandBuilder.buildCommand(executorName, event)
);
});
}
}

View File

@ -1,49 +0,0 @@
package com.alttd.velocitychat.chat_web.handlers;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.PartyManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.objects.Party;
import com.alttd.chat.web.WebHandler;
import com.alttd.chat.web.handler_class.PartyChatFromWeb;
import com.alttd.velocitychat.handlers.ChatHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
public class WebPartyChatHandler implements WebHandler<PartyChatFromWeb> {
private final ChatHandler chatHandler;
@Override
public Class<PartyChatFromWeb> type() {
return PartyChatFromWeb.class;
}
@Override
public void handle(PartyChatFromWeb event) {
int partyId;
try {
partyId = Integer.parseInt(event.getPartyId());
} catch (NumberFormatException e) {
log.error("Invalid party id: {}", event.getPartyId());
return;
}
UUID sender = event.getSender();
String message = event.getMessage();
Party party = PartyManager.getParty(sender);
if (party == null) {
log.error("Party not found for sender: {}", sender);
return;
}
if (party.getPartyId() != partyId) {
log.error("Party id mismatch: {} != {}", party.getPartyId(), partyId);
return;
}
ChatUser chatUser = ChatUserManager.getChatUser(sender);//TODO [Stijn] [2026-08-09]: Async since it can do a query
chatHandler.sendPartyMessageFromWeb(sender, party, chatUser.getDisplayName().asComponent(), message);
}
}

View File

@ -6,8 +6,6 @@ import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.managers.PartyManager;
import com.alttd.chat.managers.RegexManager;
import com.alttd.chat.objects.*;
import com.alttd.chat.objects.chat_log.ChatLogHandler;
import com.alttd.chat.objects.chat_log.mapper.chat_log.ChatLogType;
import com.alttd.chat.util.ALogger;
import com.alttd.chat.util.Utility;
import com.alttd.velocitychat.VelocityChat;
@ -16,7 +14,6 @@ import com.google.common.io.ByteStreams;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextReplacementConfig;
@ -25,10 +22,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import java.time.Duration;
import java.util.Date;
@ -36,17 +30,8 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Slf4j
public class ChatHandler {
private final ChatLogHandler chatLogHandler;
private final LuckPerms luckPerms;
public ChatHandler(ChatLogHandler chatLogHandler, LuckPerms luckPerms) {
this.chatLogHandler = chatLogHandler;
this.luckPerms = luckPerms;
}
public void privateMessage(String sender, String target, String message) {
UUID uuid = UUID.fromString(sender);
ChatUser senderUser = ChatUserManager.getChatUser(uuid);
@ -69,23 +54,15 @@ public class ChatHandler {
Placeholder.component("receiver", targetUser.getDisplayName()),
Placeholder.unparsed("receivername", player2.getUsername()),
Placeholder.component("message", GsonComponentSerializer.gson().deserialize(message)),
Placeholder.unparsed("server",
player.getCurrentServer().isPresent() ? player.getCurrentServer()
.get()
.getServerInfo()
.getName() : "Altitude"
)
);
Placeholder.unparsed("server", player.getCurrentServer().isPresent() ? player.getCurrentServer().get().getServerInfo().getName() : "Altitude"));
ServerConnection serverConnection;
//Log handled on receiving server since it is only received on one server and that's where the ignore check happens
if (player.getCurrentServer().isPresent() && player2.getCurrentServer().isPresent()) {
// redirect to the sender
serverConnection = player.getCurrentServer().get();
Component component = Utility.parseMiniMessage(Config.MESSAGESENDER
.replaceAll("<sendername>", player.getUsername())
.replaceAll("<receivername>", player2.getUsername()), Placeholders
).asComponent();
.replaceAll("<sendername>", player.getUsername())
.replaceAll("<receivername>", player2.getUsername()), Placeholders).asComponent();
ByteArrayDataOutput buf = ByteStreams.newDataOutput();
buf.writeUTF("privatemessageout");
buf.writeUTF(player.getUniqueId().toString());
@ -97,9 +74,8 @@ public class ChatHandler {
//redirect to the receiver
serverConnection = player2.getCurrentServer().get();
component = Utility.parseMiniMessage(Config.MESSAGERECIEVER
.replaceAll("<sendername>", player.getUsername())
.replaceAll("<receivername>", player2.getUsername()), Placeholders
).asComponent();
.replaceAll("<sendername>", player.getUsername())
.replaceAll("<receivername>", player2.getUsername()), Placeholders).asComponent();
buf = ByteStreams.newDataOutput();
buf.writeUTF("privatemessagein");
buf.writeUTF(player2.getUniqueId().toString());
@ -117,7 +93,7 @@ public class ChatHandler {
Placeholder.parsed("displayname", Utility.getDisplayName(player.getUniqueId(), player.getUsername())),
Placeholder.unparsed("target", (target.isEmpty() ? " tried to say: " : " -> " + target + ": ")),
Placeholder.unparsed("input", input)
);
);
ComponentLike blockedNotification = Utility.parseMiniMessage(Config.NOTIFICATIONFORMAT, Placeholders);
serverConnection.getServer().getPlayersConnected().forEach(pl -> {
@ -144,125 +120,48 @@ public class ChatHandler {
});
}
public void sendPartyMessageFromWeb(UUID uuid, Party party, Component senderName, String message) {
Utility.getOrLoadUser(uuid).thenAccept(user -> sendPartyMessageFromWeb(uuid, party, senderName, message, user));
}
public void sendPartyMessageFromWeb(UUID uuid, Party party, Component senderName, String message, User user) {
Optional<ParsedPartyMessage> optionalParsedPartyMessage = getResult(uuid,
message,
null,
null,
senderName,
party,
user.getUsername(),
user,
null
);
if (optionalParsedPartyMessage.isEmpty()) {
log.error("Failed to parse party message: {}", message);
return;
}
ParsedPartyMessage parsedPartyMessage = optionalParsedPartyMessage.get();
chatLogHandler.addChatLog(uuid,
"web",
message,
ChatLogType.PARTY,
null,
null,
parsedPartyMessage.partyMessage(),
false
);
}
public void sendPartyMessage(UUID uuid, String message, Component item, ServerConnection serverConnection) {
Utility.getOrLoadUser(uuid).thenAccept(user -> sendPartyMessage(uuid, message, item, serverConnection, user));
}
public void sendPartyMessage(UUID uuid, String message, Component item, ServerConnection serverConnection, User user) {
Optional<Player> optionalPlayer = VelocityChat.getPlugin().getProxy().getPlayer(uuid);
if (optionalPlayer.isEmpty()) {
return;
}
Player player = optionalPlayer.get();
ChatUser chatUser = ChatUserManager.getChatUser(uuid);
Party party = PartyManager.getParty(chatUser.getPartyId());
ChatUser user = ChatUserManager.getChatUser(uuid);
Party party = PartyManager.getParty(user.getPartyId());
if (party == null) {
player.sendMessage(Utility.parseMiniMessage(Config.NOT_IN_A_PARTY));
return;
}
ComponentLike senderName = chatUser.getDisplayName();
ComponentLike senderName = user.getDisplayName();
Optional<ParsedPartyMessage> optionalParsedPartyMessage = getResult(uuid,
message,
item,
serverConnection,
senderName,
party,
player.getUsername(),
user,
player
);
if (optionalParsedPartyMessage.isEmpty()) {
TagResolver Placeholders = TagResolver.resolver(
Placeholder.component("sender", senderName),
Placeholder.component("sendername", senderName),
Placeholder.unparsed("partyname", party.getPartyName()),
Placeholder.component("message", parseMessageContent(player, message)),
Placeholder.unparsed("server", serverConnection.getServer().getServerInfo().getName())
);
Component partyMessage = Utility.parseMiniMessage(Config.PARTY_FORMAT, Placeholders).asComponent()
.replaceText(TextReplacementConfig.builder().once().matchLiteral("[i]").replacement(item).build());
ModifiableString modifiableString = new ModifiableString(partyMessage);
if (!RegexManager.filterText(player.getUsername(), uuid, modifiableString, "party")) {
sendBlockedNotification("Party Language", player, message, "", serverConnection);
return; // the message was blocked
}
ParsedPartyMessage parsedPartyMessage = optionalParsedPartyMessage.get();
partyMessage = modifiableString.component();
sendPartyMessage(party, parsedPartyMessage.partyMessage(), chatUser.getIgnoredBy());
sendPartyMessage(party, partyMessage, user.getIgnoredBy());
chatLogHandler.addChatLog(uuid,
serverConnection.getServer().getServerInfo().getName(),
PlainTextComponentSerializer.plainText().serialize(parsedPartyMessage.partyMessage()),
ChatLogType.PARTY,
String.valueOf(party.getPartyId()),
null,
parsedPartyMessage.partyMessage(),
false
);
ComponentLike spyMessage = Utility.parseMiniMessage(Config.PARTY_SPY, parsedPartyMessage.placeholders());
ComponentLike spyMessage = Utility.parseMiniMessage(Config.PARTY_SPY, Placeholders);
for (Player pl : serverConnection.getServer().getPlayersConnected()) {
if (pl.hasPermission(Config.SPYPERMISSION) && !party.getPartyUsersUuid().contains(pl.getUniqueId())) {
pl.sendMessage(spyMessage);
}
}
ALogger.info(PlainTextComponentSerializer.plainText().serialize(parsedPartyMessage.partyMessage()));
}
private Optional<ParsedPartyMessage> getResult(UUID uuid, String message, @Nullable Component item,
@Nullable ServerConnection serverConnection, ComponentLike senderName,
Party party, String playerName, User user, @Nullable Player player) {
TagResolver placeholders = TagResolver.resolver(
Placeholder.component("sender", senderName),
Placeholder.component("sendername", senderName),
Placeholder.unparsed("partyname", party.getPartyName()),
Placeholder.component("message", parseMessageContent(user, message)),
Placeholder.unparsed("server",
serverConnection != null ? serverConnection.getServer().getServerInfo().getName() : "web"
)
);
Component partyMessage;
if (item != null) {
partyMessage = Utility.parseMiniMessage(Config.PARTY_FORMAT, placeholders).asComponent()
.replaceText(TextReplacementConfig.builder().once().matchLiteral("[i]").replacement(item).build());
} else {
partyMessage = Utility.parseMiniMessage(Config.PARTY_FORMAT, placeholders).asComponent();
}
ModifiableString modifiableString = new ModifiableString(partyMessage);
if (!RegexManager.filterText(playerName, uuid, modifiableString, "party")) {
if (serverConnection != null && player != null) {
sendBlockedNotification("Party Language", player, message, "", serverConnection);
}
return Optional.empty();
}
partyMessage = modifiableString.component();
return Optional.of(new ParsedPartyMessage(placeholders, partyMessage));
ALogger.info(PlainTextComponentSerializer.plainText().serialize(partyMessage));
}
public void globalAdminChat(String message) {
@ -272,39 +171,24 @@ public class ChatHandler {
.stream()
.filter(target -> target.hasPermission("command.chat.globaladminchat"))
.forEach(target -> target.sendMessage(component));
chatLogHandler.addChatLog(
new UUID(0, 0), //No origin uuid for GAC
"GAC", //No server for GAC
PlainTextComponentSerializer.plainText().serialize(component),
ChatLogType.GAC,
null,
null,
component,
false
);
}
public void globalAdminChat(CommandSource commandSource, String message) {
ComponentLike senderName = Component.text(Config.CONSOLENAME);
String serverName = "Altitude";
if (commandSource instanceof Player sender) {
ChatUser chatUser = ChatUserManager.getChatUser(sender.getUniqueId());
if (chatUser == null) {
ChatUser user = ChatUserManager.getChatUser(sender.getUniqueId());
if (user == null) {
return;
}
senderName = chatUser.getDisplayName();
serverName = sender.getCurrentServer().isPresent() ? sender.getCurrentServer()
.get()
.getServerInfo()
.getName() : "Altitude";
senderName = user.getDisplayName();
serverName = sender.getCurrentServer().isPresent() ? sender.getCurrentServer().get().getServerInfo().getName() : "Altitude";
}
TagResolver Placeholders = TagResolver.resolver(
Placeholder.component("message", parseMessageContent(commandSource, message)),
Placeholder.component("sender", senderName),
Placeholder.unparsed("server", serverName)
);
Placeholder.unparsed("server", serverName));
ComponentLike component = Utility.parseMiniMessage(Config.GACFORMAT, Placeholders);
@ -326,8 +210,7 @@ public class ChatHandler {
if (optionalPlayer.isEmpty()) {
targetUUID = ServerHandler.getPlayerUUID(recipient);
if (targetUUID == null) {
commandSource.sendMessage(Utility.parseMiniMessage(
"<red>A player with this name hasn't logged in recently.")); // TOOD load from config
commandSource.sendMessage(Utility.parseMiniMessage("<red>A player with this name hasn't logged in recently.")); // TOOD load from config
return;
}
} else {
@ -391,10 +274,8 @@ public class ChatHandler {
Placeholder.component("sender", chatUser.getDisplayName()),
Placeholder.component("message", Utility.parseMiniMessage(mail.getMessage())),
Placeholder.unparsed("date", date.toString()),
Placeholder.unparsed("time_ago",
getTimeAgo(Duration.between(date.toInstant(), new Date().toInstant()))
)
);
Placeholder.unparsed("time_ago", getTimeAgo(Duration.between(date.toInstant(), new Date().toInstant())))
);
ComponentLike mailMessage = Utility.parseMiniMessage(Config.mailBody, Placeholders);
component = component.append(Component.newline()).append(mailMessage);
}
@ -421,31 +302,15 @@ public class ChatHandler {
return stringBuilder.toString();
}
private Component parseMessageContent(User user, String rawMessage) {
private Component parseMessageContent(CommandSource source, String rawMessage) {
TagResolver.Builder tagResolver = TagResolver.builder();
Utility.formattingPerms.forEach((perm, pair) -> {
if (Utility.hasPermission(user, perm)) {
if (source.hasPermission(perm)) {
tagResolver.resolver(pair.getX());
}
});
return getComponent(rawMessage, tagResolver);
}
private Component parseMessageContent(CommandSource commandSource, String rawMessage) {
TagResolver.Builder tagResolver = TagResolver.builder();
Utility.formattingPerms.forEach((perm, pair) -> {
if (commandSource.hasPermission(perm)) {
tagResolver.resolver(pair.getX());
}
});
return getComponent(rawMessage, tagResolver);
}
private static @NonNull Component getComponent(String rawMessage, TagResolver.Builder tagResolver) {
MiniMessage miniMessage = MiniMessage.builder().tags(tagResolver.build()).build();
Component component = miniMessage.deserialize(rawMessage);
for (ChatFilter chatFilter : RegexManager.getEmoteFilters()) {
@ -457,5 +322,6 @@ public class ChatHandler {
}
return component;
}
}

View File

@ -1,8 +0,0 @@
package com.alttd.velocitychat.handlers;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
public record ParsedPartyMessage(TagResolver placeholders, Component partyMessage) {
}

View File

@ -1,235 +0,0 @@
package com.alttd.velocitychat.chat_web;
import com.alttd.chat.web.handler_class.PunishFromWeb;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PunishmentCommandBuilderTest {
private PunishFromWeb baseEvent(String type, UUID executorUuid, UUID targetUuid) {
PunishFromWeb event = new PunishFromWeb();
event.setExecutor(executorUuid);
event.setTarget(targetUuid);
event.setType(type);
return event;
}
// ---- BAN ----
@Test
public void testBuildBanCommandWithTime() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("ban", executorUuid, targetUuid);
event.setTime("P7D"); // ISO-8601 duration: 7 days
event.setReason("Griefing");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "ban " + targetUuid + " 7d --sender=ExecutorName --sender-uuid=" + executorUuid + " Griefing";
assertEquals(expected, command);
}
@Test
public void testBuildBanCommandPermanentWhenNoTime() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("ban", executorUuid, targetUuid);
event.setTime(null);
event.setReason("Griefing");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "ban " + targetUuid + " --sender=ExecutorName --sender-uuid=" + executorUuid + " Griefing";
assertEquals(expected, command);
}
@Test
public void testBuildBanCommandPermanentWhenBlankTime() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("ban", executorUuid, targetUuid);
event.setTime(" ");
event.setReason("Griefing");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "ban " + targetUuid + " --sender=ExecutorName --sender-uuid=" + executorUuid + " Griefing";
assertEquals(expected, command);
}
@Test
public void testBuildBanCommandMissingReasonThrows() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("ban", executorUuid, targetUuid);
event.setTime(" ");
event.setReason(" ");
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.buildCommand("ExecutorName", event)
);
}
// ---- MUTE ----
@Test
public void testBuildMuteCommand() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("mute", executorUuid, targetUuid);
event.setTime("PT30M"); // 30 minutes
event.setReason("Spamming");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "mute " + targetUuid + " 30m --sender=ExecutorName --sender-uuid=" + executorUuid + " Spamming";
assertEquals(expected, command);
}
@Test
public void testBuildMuteCommandNoTimeThrows() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("mute", executorUuid, targetUuid);
event.setTime(null);
event.setReason("Spamming");
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.buildCommand("ExecutorName", event)
);
}
@Test
public void testBuildMuteCommandBlankTimeThrows() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("mute", executorUuid, targetUuid);
event.setTime(" ");
event.setReason("Spamming");
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.buildCommand("ExecutorName", event)
);
}
// ---- WARN ----
@Test
public void testBuildWarnCommandAlwaysUses30d() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("warn", executorUuid, targetUuid);
event.setTime(null); // not supplied for warns, should default to 30d anyway
event.setReason("Bad attitude");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "warn " + targetUuid + " 30d --sender=ExecutorName --sender-uuid=" + executorUuid + " Bad attitude";
assertEquals(expected, command);
}
@Test
public void testBuildWarnCommandIgnoresSuppliedTime() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("warn", executorUuid, targetUuid);
event.setTime("P1D"); // should be ignored; warns are always 30d
event.setReason("Bad attitude");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "warn " + targetUuid + " 30d --sender=ExecutorName --sender-uuid=" + executorUuid + " Bad attitude";
assertEquals(expected, command);
}
@Test
public void testBuildWarnCommandNoReasonThrows() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("warn", executorUuid, targetUuid);
event.setTime(null);
event.setReason("");
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.buildCommand("ExecutorName", event)
);
}
// ---- Type validation ----
@Test
public void testInvalidTypeThrows() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("kick", executorUuid, targetUuid);
event.setReason("Bad attitude");
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.buildCommand("ExecutorName", event)
);
}
@Test
public void testTypeIsCaseInsensitive() {
UUID executorUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
PunishFromWeb event = baseEvent("BAN", executorUuid, targetUuid);
event.setTime(null);
event.setReason("Griefing");
String command = PunishmentCommandBuilder.buildCommand("ExecutorName", event);
String expected = "ban " + targetUuid + " --sender=ExecutorName --sender-uuid=" + executorUuid + " Griefing";
assertEquals(expected, command);
}
// ---- Duration parsing ----
@Test
public void testParseDurationDaysOnly() {
assertEquals("7d", PunishmentCommandBuilder.parseDuration("P7D"));
}
@Test
public void testParseDurationMinutesOnly() {
assertEquals("45m", PunishmentCommandBuilder.parseDuration("PT45M"));
}
@Test
public void testParseDurationTruncatesToDaysWhenDaysPresent() {
// days take priority over everything smaller, which is dropped entirely
assertEquals("1d", PunishmentCommandBuilder.parseDuration("P1DT2H3M4S"));
}
@Test
public void testParseDurationTruncatesToHoursWhenNoDays() {
// hours take priority over minutes, which is dropped
assertEquals("2h", PunishmentCommandBuilder.parseDuration("PT2H3M"));
}
@Test
public void testParseDurationInvalidFormatThrows() {
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.parseDuration("7d")
);
}
@Test
public void testParseDurationZeroThrows() {
// duration must be positive
assertThrows(IllegalArgumentException.class,
() -> PunishmentCommandBuilder.parseDuration("PT0S")
);
}
@Test
public void testParseDurationSubMinuteThrows() {
// litebans has no second-level precision defaults to 1m
assertEquals("1m", PunishmentCommandBuilder.parseDuration("PT30S"));
}
}

View File

@ -1,66 +0,0 @@
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"
)
)
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

@ -1,144 +0,0 @@
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
# TODO [Stijn] [2026-07-19]: Add who they ignore and who they are ignored by
properties:
uuid:
type: string
format: uuid
name:
type: string
styledName:
type: string
ChatMessage:
type: object
required:
- uuid
- message
- server
- type
- timestamp
- blocked
properties:
uuid:
type: string
format: uuid
message:
type: string
server:
type: string
type:
type: string
enum:
- PUBLIC
- GLOBAL
- PARTY
- GAC
- MSG
- CUSTOM
channel:
type: string
maxLength: 36
receiver:
type: string
maxLength: 36
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

@ -1,22 +0,0 @@
{
"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
}