chore(release): v0.2

Added the `/r` command for quick replies, implemented logging for private and global messages, and enabled clickable nicknames and links in chat.
This commit is contained in:
itqop 2024-11-10 18:27:25 +03:00
parent f14c4e3fc0
commit 8fc7b9a5f9
6 changed files with 279 additions and 15 deletions

View File

@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/).
## [0.2] - 2024-11-10
### Added
- **ChatITCommand.java**:
- **`/r` Command**:
- Introduced the `/r` command for quick replies to the last received private message.
- Allows players to send responses without specifying the recipient's name.
- **Utils.java**:
- **Message Logging**:
- Implemented logging for private and global messages into separate files: `logs/private_messages.log` and `logs/global_messages.log`.
- Each message is logged with timestamp, sender's IP address, player names, and message content.
- **Clickable Nicknames and Links in Chat**:
- Added clickable player nicknames in chat. Clicking on a player's nickname suggests sending a private message to them.
- Enabled clickable URLs in chat messages. Valid URLs are automatically converted into clickable links that open in the player's browser upon clicking.
- Implemented URL validation before converting them into clickable components.
## [0.1.9] - 2024-10-31
### Added

View File

@ -4,6 +4,26 @@
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/), и этот проект придерживается [Семантического Версионирования](https://semver.org/lang/ru/).
## [0.2] - 2024-11-10
### Добавлено
- **ChatITCommand.java**:
- **Команда `/r`**:
- Добавлена команда `/r` для быстрого ответа на последнее полученное приватное сообщение.
- Позволяет игрокам быстро отправлять ответы без необходимости указывать имя получателя.
- **Utils.java**:
- **Логирование сообщений**:
- Реализовано логирование приватных и глобальных сообщений в отдельные файлы `logs/private_messages.log` и `logs/global_messages.log`.
- Каждое сообщение логируется с указанием времени, IP-адреса отправителя, имен игроков и содержания сообщения.
- **Кликабельность ников и ссылок в чате**:
- Добавлена возможность кликабельности ников игроков в чате. При клике на ник игрока предлагается отправить приватное сообщение.
- Внедрена кликабельность ссылок в сообщениях чата. Валидные URL автоматически преобразуются в кликабельные ссылки, которые открываются в браузере игрока при клике.
- Реализована проверка валидности ссылок перед их преобразованием в кликабельные компоненты.
## [0.1.9] - 2024-10-31
### Добавлено

View File

@ -38,7 +38,7 @@ mod_name=ChatIT
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved
# The mod version. See https://semver.org/
mod_version=0.1.9-BETA
mod_version=0.2-BETA
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -10,16 +10,27 @@ import net.minecraft.commands.arguments.MessageArgument;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent;
import org.itqop.chatit.utils.PlayerConfigManager;
import org.itqop.chatit.utils.Config;
import org.itqop.chatit.utils.ProfanityMode;
import static org.itqop.chatit.utils.Utils.logPrivateMessage;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@Mod.EventBusSubscriber
public class ChatITCommand {
private static final Map<UUID, UUID> lastSenders = new ConcurrentHashMap<>();
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
@ -36,7 +47,6 @@ public class ChatITCommand {
return 1;
})
)
// Добавление подкоманды mode
.then(Commands.literal("mode")
.requires(source -> source.hasPermission(2)) // Требует уровень разрешения 2 (оператор)
.then(Commands.argument("mode", StringArgumentType.word())
@ -67,6 +77,31 @@ public class ChatITCommand {
)
);
// Регистрация команды /r
dispatcher.register(Commands.literal("r")
.then(Commands.argument("message", MessageArgument.message())
.executes(context -> {
ServerPlayer sender = context.getSource().getPlayerOrException();
UUID lastSenderUUID = lastSenders.get(sender.getUUID());
if (lastSenderUUID == null) {
context.getSource().sendFailure(Component.literal("Нет игрока для ответа.")
.withStyle(ChatFormatting.RED));
return 0;
}
ServerPlayer target = context.getSource().getServer().getPlayerList().getPlayer(lastSenderUUID);
if (target == null) {
context.getSource().sendFailure(Component.literal("Последний отправитель недоступен.")
.withStyle(ChatFormatting.RED));
return 0;
}
Component message = MessageArgument.getMessage(context, "message");
return sendPrivateMessage(context.getSource(), target, message);
})
)
);
}
public static void registerPrivateMessageCommand(CommandDispatcher<CommandSourceStack> dispatcher, String commandName) {
@ -90,15 +125,15 @@ public class ChatITCommand {
* @return Статус выполнения команды.
*/
private static int sendPrivateMessage(CommandSourceStack source, ServerPlayer target, Component message) {
// Проверка, что источник команды является игроком
if (!(source.getEntity() instanceof ServerPlayer sender)) {
source.sendFailure(Component.literal("Только игроки могут отправлять личные сообщения."));
source.sendFailure(Component.literal("Только игроки могут отправлять личные сообщения.")
.withStyle(ChatFormatting.RED));
return 0;
}
// Проверка, что отправитель не отправляет сообщение самому себе
if (sender.getUUID().equals(target.getUUID())) {
sender.sendSystemMessage(Component.literal("Вы не можете отправлять сообщения самому себе."));
sender.sendSystemMessage(Component.literal("Вы не можете отправлять сообщения самому себе.")
.withStyle(ChatFormatting.RED));
return 0;
}
@ -124,6 +159,26 @@ public class ChatITCommand {
target.sendSystemMessage(messageFrom);
sender.sendSystemMessage(messageTo);
lastSenders.put(target.getUUID(), sender.getUUID());
logPrivateMessage(sender, target, message);
return 1;
}
/**
* Слушатель события отключения игрока для очистки хэшмапы.
*
* @param event Событие отключения игрока.
*/
@SubscribeEvent
public static void onPlayerLogout(PlayerLoggedOutEvent event) {
Player player = event.getEntity();
UUID playerUUID = player.getUUID();
lastSenders.values().removeIf(senderUUID -> senderUUID.equals(playerUUID));
lastSenders.remove(playerUUID);
}
}

View File

@ -1,5 +1,7 @@
package org.itqop.chatit.handlers;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.HoverEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.event.ServerChatEvent;
import net.minecraftforge.fml.common.Mod;
@ -15,16 +17,27 @@ import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.itqop.chatit.utils.Utils.isValidUrl;
import static org.itqop.chatit.utils.Utils.logGlobalMessage;
@Mod.EventBusSubscriber
public class ChatEventHandler {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Pattern URL_PATTERN = Pattern.compile(
"(https?://\\S+)", Pattern.CASE_INSENSITIVE
);
@SubscribeEvent
public static void onServerChat(ServerChatEvent event) {
ServerPlayer sender = event.getPlayer();
MutableComponent errorComponent = createErrorMessage(Config.messageLocal);
MutableComponent adultComponent = createAdultMessage();
ServerPlayer sender = event.getPlayer();
MinecraftServer server = sender.getServer();
String originalMessage = event.getMessage().getString();
@ -37,6 +50,8 @@ public class ChatEventHandler {
MutableComponent chatComponent = createChatMessage(sender, message, isGlobal);
logGlobalMessage(chatComponent);
CompletableFuture<Double> future = ProfanityChecker.checkMessageAsync(message);
future.thenAccept(profanityScore -> {
@ -45,7 +60,6 @@ public class ChatEventHandler {
double threshold = Config.profanityThreshold;
if (profanityScore > threshold && !PlayerConfigManager.hasAdultContentEnabled(sender)) {
MutableComponent adultComponent = createAdultMessage();
sender.sendSystemMessage(adultComponent);
} else {
boolean localMessageSent = false;
@ -73,7 +87,6 @@ public class ChatEventHandler {
if (!isGlobal) {
if (!localMessageSent) {
MutableComponent errorComponent = createErrorMessage(Config.messageLocal);
sender.sendSystemMessage(errorComponent);
} else {
sender.sendSystemMessage(chatComponent);
@ -86,8 +99,8 @@ public class ChatEventHandler {
LOGGER.error(ex.toString());
if (server != null) {
server.execute(() -> {
MutableComponent errorComponent = createErrorMessage(message);
sender.sendSystemMessage(errorComponent);
MutableComponent errorCustomComponent = createErrorMessage(message);
sender.sendSystemMessage(errorCustomComponent);
});
}
return null;
@ -120,6 +133,14 @@ public class ChatEventHandler {
return prefixComponent.append(errorMessage);
}
/**
* Метод для создания сообщения чата с кликабельными ссылками.
*
* @param player Игрок, отправляющий сообщение.
* @param message Текст сообщения.
* @param isGlobal Флаг, указывающий на глобальное или локальное сообщение.
* @return Кликабельное сообщение чата.
*/
private static MutableComponent createChatMessage(ServerPlayer player, String message, boolean isGlobal) {
MutableComponent openBracket = Component.literal("[");
MutableComponent letterComponent;
@ -135,10 +156,47 @@ public class ChatEventHandler {
MutableComponent prefixComponent = openBracket.append(letterComponent).append(closeBracket);
Component playerName = Component.literal(player.getName().getString());
Component separatorComponent = Component.literal(": ");
Component messageComponent = Component.literal(message).withStyle(ChatFormatting.YELLOW);
MutableComponent playerName = Component.literal(player.getName().getString())
.withStyle(style -> style
.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/msg " + player.getName().getString() + " "))
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("Нажмите, чтобы ответить")))
.withColor(ChatFormatting.WHITE)
);
return prefixComponent.append(playerName).append(separatorComponent).append(messageComponent);
Component separatorComponent = Component.literal(": ");
MutableComponent messageComponent = Component.empty();
Matcher matcher = URL_PATTERN.matcher(message);
int lastEnd = 0;
while (matcher.find()) {
if (matcher.start() > lastEnd) {
String textBeforeUrl = message.substring(lastEnd, matcher.start());
messageComponent.append(Component.literal(textBeforeUrl));
}
String url = matcher.group(1);
if (isValidUrl(url)) {
MutableComponent clickableLink = Component.literal(url)
.withStyle(style -> style
.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url))
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("Нажмите, чтобы открыть ссылку")))
.withColor(ChatFormatting.BLUE)
.withUnderlined(true)
);
messageComponent.append(clickableLink);
} else {
messageComponent.append(Component.literal(url));
}
lastEnd = matcher.end();
}
if (lastEnd < message.length()) {
String remainingText = message.substring(lastEnd);
messageComponent.append(Component.literal(remainingText));
}
return prefixComponent.append(playerName).append(separatorComponent).append(messageComponent.withStyle(ChatFormatting.YELLOW));
}
}

View File

@ -0,0 +1,112 @@
package org.itqop.chatit.utils;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.net.URL;
public class Utils {
private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final String PRIVATE_MESSAGES_LOG = "logs/private_messages.log";
private static final String GLOBAL_MESSAGES_LOG = "logs/global_messages.log";
/**
* Метод для получения IP-адреса игрока.
*
* @param player Игрок, чей IP требуется получить.
* @return IP-адрес игрока в строковом формате.
*/
private static String getPlayerIp(ServerPlayer player) {
ServerGamePacketListenerImpl connection = player.connection;
InetSocketAddress address = (InetSocketAddress) connection.connection.getRemoteAddress();
InetAddress inetAddress = address.getAddress();
if (inetAddress == null) {
return "Unknown IP";
}
if (inetAddress instanceof Inet4Address) {
return inetAddress.getHostAddress();
} else if (inetAddress instanceof Inet6Address) {
String ip = inetAddress.getHostAddress();
int percentIndex = ip.indexOf('%');
if (percentIndex != -1) {
ip = ip.substring(0, percentIndex);
}
return ip;
} else {
return inetAddress.getHostAddress();
}
}
/**
* Метод для логирования приватных сообщений в файл.
*
* @param sender Игрок, отправляющий сообщение.
* @param target Целевой игрок.
* @param message Текст сообщения.
*/
public static void logPrivateMessage(ServerPlayer sender, ServerPlayer target, Component message) {
String time = LocalDateTime.now().format(LOG_TIME_FORMAT);
String senderIp = getPlayerIp(sender);
String senderName = sender.getName().getString();
String targetName = target.getName().getString();
String messageText = message.getString();
String logEntry = String.format("[%s] [%s] - %s -> %s: %s%n", time, senderIp, senderName, targetName, messageText);
try (BufferedWriter writer = new BufferedWriter(new FileWriter(PRIVATE_MESSAGES_LOG, true))) {
writer.write(logEntry);
} catch (IOException e) {
//LOGGER.error("Не удалось записать приватное сообщение в лог.", e);
}
}
/**
* Метод для логирования глобальных сообщений в файл.
*
* @param message Глобальное сообщение в формате MutableComponent.
*/
public static void logGlobalMessage(MutableComponent message) {
String time = LocalDateTime.now().format(LOG_TIME_FORMAT);
String messageText = message.getString();
String logEntry = String.format("[%s] %s%n", time, messageText);
try (BufferedWriter writer = new BufferedWriter(new FileWriter(GLOBAL_MESSAGES_LOG, true))) {
writer.write(logEntry);
} catch (IOException e) {
//LOGGER.error("Не удалось записать глобальное сообщение в лог.", e);
}
}
/**
* Метод для проверки валидности URL.
* В будущем сюда можно добавить дополнительные проверки, например, DGA.
*
* @param url Строка с URL для проверки.
* @return true, если URL валиден, иначе false.
*/
public static boolean isValidUrl(String url) {
try {
new URL(url).toURI(); // Проверяем синтаксис URL
return true;
} catch (Exception e) {
return false;
}
}
}