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 =