From c49bcd0f841a410fa3185bc54cad68933c4945c0 Mon Sep 17 00:00:00 2001 From: "Andrei.Ovcharenko" Date: Sun, 16 Aug 2026 12:11:27 +0300 Subject: [PATCH] Let the window close, answer stray handshake frames, reply where asked - Closing the chat window while the connection dialog is open left the thread inside requestConnectionSettings blocked on its latch forever, so the JVM never exited. windowClosing now completes the pending request the same way Cancel does. - NAME_REQUEST and USER_NAME were the only frame types left to the default branch of the server loop, so a stray handshake frame dropped the connection with an IOException while every other misplaced frame got a soft "Unsupported client frame" error. - The bot answered every command with a plain text message to the general room: a private question got a public answer, and a question asked in a room was answered elsewhere. It now replies in the room it was asked in, or privately to the sender of a private command. - ci.yml pinned gradle-version 8.10.2 for setup-gradle although the build runs through the 9.7 wrapper, so that download was dead weight (codeql.yml already omits it). Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 -- .../networkchat/client/BotChatClient.java | 23 ++++++++++++++++-- .../networkchat/client/gui/ChatWindow.java | 2 ++ .../networkchat/network/ChatServer.java | 5 ++-- .../networkchat/client/BotChatClientTest.java | 24 +++++++++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba21aaa..639a742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,6 @@ jobs: uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: Setup Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - with: - gradle-version: "8.10.2" - name: Run tests and checks run: ./gradlew check jacocoAllReport --stacktrace - name: Publish coverage summary diff --git a/src/main/java/dev/krotname/networkchat/client/BotChatClient.java b/src/main/java/dev/krotname/networkchat/client/BotChatClient.java index 0300e30..14740e6 100644 --- a/src/main/java/dev/krotname/networkchat/client/BotChatClient.java +++ b/src/main/java/dev/krotname/networkchat/client/BotChatClient.java @@ -1,6 +1,7 @@ package dev.krotname.networkchat.client; import dev.krotname.networkchat.protocol.ChatMessage; +import dev.krotname.networkchat.protocol.MessageType; import java.io.IOException; import java.time.Clock; import java.time.LocalDateTime; @@ -85,6 +86,23 @@ String answerForCommand(ChatMessage message) { return String.format("Информация для %s: %s", message.sender(), answer); } + /** + * Answers where the command was asked: privately to the sender of a private command, otherwise in + * the room the command came from. Replying with a plain text message would publish the answer to + * a private question in the general room. + */ + ChatMessage replyTo(ChatMessage message, String answer) { + String sender = getResolvedUserName() == null ? botUserName : getResolvedUserName(); + if (message.type() == MessageType.PRIVATE_TEXT) { + return ChatMessage.privateText(answer, sender, message.sender()); + } + String room = message.room(); + if (room == null || room.isBlank()) { + return ChatMessage.text(answer, sender); + } + return ChatMessage.roomText(answer, sender, room); + } + private final class BotSocketThread extends SocketThread { private static final String GREETING = "Привет чатику. Я бот. Понимаю команды: дата, день, месяц, год, время, час, минуты, секунды."; @@ -98,9 +116,10 @@ protected void clientHandshake() throws IOException { @Override protected void processIncomingMessage(ChatMessage message) { String answer = answerForCommand(message); - if (answer != null) { - sendTextMessage(answer); + if (answer == null) { + return; } + sendMessage(replyTo(message, answer)); } @Override diff --git a/src/main/java/dev/krotname/networkchat/client/gui/ChatWindow.java b/src/main/java/dev/krotname/networkchat/client/gui/ChatWindow.java index 451acc3..e24af70 100644 --- a/src/main/java/dev/krotname/networkchat/client/gui/ChatWindow.java +++ b/src/main/java/dev/krotname/networkchat/client/gui/ChatWindow.java @@ -152,6 +152,8 @@ private void init(boolean visible) { new WindowAdapter() { @Override public void windowClosing(WindowEvent event) { + // Otherwise a thread waiting in requestConnectionSettings never wakes up. + cancelConnectionSettings(); controller.disconnect(); } }); diff --git a/src/main/java/dev/krotname/networkchat/network/ChatServer.java b/src/main/java/dev/krotname/networkchat/network/ChatServer.java index c67fd90..baa8d9f 100644 --- a/src/main/java/dev/krotname/networkchat/network/ChatServer.java +++ b/src/main/java/dev/krotname/networkchat/network/ChatServer.java @@ -357,7 +357,9 @@ private void serverMainLoop(ChatConnection connection, String userName, ClientLi case PRIVATE_TEXT -> handlePrivateText(message, userName, connection); case ROOM_JOIN -> handleRoomJoin(message, userName, connection, limits); case ROOM_LEAVE -> handleRoomLeave(message, userName, connection); - case NAME_ACCEPTED, + case NAME_REQUEST, + USER_NAME, + NAME_ACCEPTED, USER_ADDED, USER_REMOVED, ROOM_ADDED, @@ -367,7 +369,6 @@ private void serverMainLoop(ChatConnection connection, String userName, ClientLi ERROR -> connection.send( ChatMessage.withData(MessageType.ERROR, "Unsupported client frame", null)); - default -> throw new IOException("Unsupported message type: " + message.type()); } } } diff --git a/src/test/java/dev/krotname/networkchat/client/BotChatClientTest.java b/src/test/java/dev/krotname/networkchat/client/BotChatClientTest.java index da54dc6..b7b9c33 100644 --- a/src/test/java/dev/krotname/networkchat/client/BotChatClientTest.java +++ b/src/test/java/dev/krotname/networkchat/client/BotChatClientTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import dev.krotname.networkchat.protocol.ChatMessage; +import dev.krotname.networkchat.protocol.MessageType; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; @@ -21,6 +22,29 @@ void answersCommandUsingStructuredSender() { assertEquals("Информация для alice: 2026", answer); } + @Test + void answersPrivateCommandsPrivately() { + BotChatClient bot = + new BotChatClient(Clock.fixed(Instant.parse("2026-06-10T12:34:56Z"), ZoneId.of("UTC"))); + + ChatMessage reply = bot.replyTo(ChatMessage.privateText("год", "alice", "bot"), "answer"); + + assertEquals(MessageType.PRIVATE_TEXT, reply.type()); + assertEquals("alice", reply.recipient()); + assertEquals("answer", reply.data()); + } + + @Test + void answersRoomCommandsInTheSameRoom() { + BotChatClient bot = + new BotChatClient(Clock.fixed(Instant.parse("2026-06-10T12:34:56Z"), ZoneId.of("UTC"))); + + ChatMessage reply = bot.replyTo(ChatMessage.roomText("год", "alice", "team"), "answer"); + + assertEquals(MessageType.ROOM_TEXT, reply.type()); + assertEquals("team", reply.room()); + } + @Test void ignoresUnknownOrSenderlessCommands() { BotChatClient bot =