diff --git a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java index 0fb0dc1..5e5db90 100644 --- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java +++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java @@ -11,12 +11,8 @@ import lombok.Getter; import lombok.Setter; import nl.andrewl.concord_client.gui.MainWindow; import nl.andrewl.concord_core.msg.Message; -import nl.andrewl.concord_core.msg.MessageUtils; import nl.andrewl.concord_core.msg.Serializer; -import nl.andrewl.concord_core.msg.types.Chat; -import nl.andrewl.concord_core.msg.types.Identification; -import nl.andrewl.concord_core.msg.types.ServerMetaData; -import nl.andrewl.concord_core.msg.types.ServerWelcome; +import nl.andrewl.concord_core.msg.types.*; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -28,7 +24,6 @@ import java.util.Set; import java.util.UUID; public class ConcordClient implements Runnable { - private final Socket socket; private final DataInputStream in; private final DataOutputStream out; @@ -53,6 +48,10 @@ public class ConcordClient implements Runnable { this.id = welcome.getClientId(); this.currentChannelId = welcome.getCurrentChannelId(); this.serverMetaData = welcome.getMetaData(); + + // Start fetching initial data for the channel we were initially put into. + this.sendMessage(new ChannelUsersRequest(this.currentChannelId)); + this.sendMessage(new ChatHistoryRequest(this.currentChannelId, ChatHistoryRequest.Source.CHANNEL, "")); } else { throw new IOException("Unexpected response from the server after sending identification message."); } diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelChatBox.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelChatBox.java index 7732c8e..359804c 100644 --- a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelChatBox.java +++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelChatBox.java @@ -10,11 +10,17 @@ import nl.andrewl.concord_client.ConcordClient; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +/** + * This panel occupies the center of the interface, and displays the list of + * recent messages, along with an input text box for the user to type messages + * into. + */ public class ChannelChatBox extends Panel { private final ConcordClient client; private Border chatBorder; @Getter private final ChatList chatList; + @Getter private final TextBox inputTextBox; public ChannelChatBox(ConcordClient client, Window window) { super(new BorderLayout()); diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java index 7891571..aad5eb6 100644 --- a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java +++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java @@ -1,9 +1,6 @@ package nl.andrewl.concord_client.gui; -import com.googlecode.lanterna.gui2.Button; -import com.googlecode.lanterna.gui2.Direction; -import com.googlecode.lanterna.gui2.LinearLayout; -import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.*; import nl.andrewl.concord_client.ConcordClient; import nl.andrewl.concord_core.msg.types.MoveToChannel; @@ -11,7 +8,6 @@ import java.io.IOException; public class ChannelList extends Panel { private final ConcordClient client; - public ChannelList(ConcordClient client) { super(new LinearLayout(Direction.VERTICAL)); this.client = client; @@ -25,11 +21,12 @@ public class ChannelList extends Panel { name = "*" + name; } Button b = new Button(name, () -> { - System.out.println("Sending request to go to channel " + channel.getName()); - try { - client.sendMessage(new MoveToChannel(channel.getId())); - } catch (IOException e) { - e.printStackTrace(); + if (!client.getCurrentChannelId().equals(channel.getId())) { + try { + client.sendMessage(new MoveToChannel(channel.getId())); + } catch (IOException e) { + e.printStackTrace(); + } } }); this.addComponent(b, LinearLayout.createLayoutData(LinearLayout.Alignment.End)); diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChatPanel.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChatPanel.java index a7c91c6..b2ce040 100644 --- a/client/src/main/java/nl/andrewl/concord_client/gui/ChatPanel.java +++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChatPanel.java @@ -5,8 +5,7 @@ import lombok.Getter; import nl.andrewl.concord_client.ClientMessageListener; import nl.andrewl.concord_client.ConcordClient; import nl.andrewl.concord_core.msg.Message; -import nl.andrewl.concord_core.msg.types.Chat; -import nl.andrewl.concord_core.msg.types.MoveToChannel; +import nl.andrewl.concord_core.msg.types.*; import java.io.IOException; @@ -24,16 +23,16 @@ public class ChatPanel extends Panel implements ClientMessageListener { private final UserList userList; private final ConcordClient client; + private final TextGUIThread guiThread; public ChatPanel(ConcordClient client, Window window) { super(new BorderLayout()); + this.guiThread = window.getTextGUI().getGUIThread(); this.client = client; this.channelChatBox = new ChannelChatBox(client, window); this.channelList = new ChannelList(client); this.channelList.setChannels(); - this.userList = new UserList(); - this.userList.addItem("andrew"); - this.userList.addItem("tester"); + this.userList = new UserList(client); Border b; b = Borders.doubleLine("Channels"); @@ -53,9 +52,25 @@ public class ChatPanel extends Panel implements ClientMessageListener { this.channelChatBox.getChatList().addItem(chat); } else if (message instanceof MoveToChannel moveToChannel) { client.setCurrentChannelId(moveToChannel.getChannelId()); - this.channelList.setChannels(); - this.channelChatBox.getChatList().clearItems(); - this.channelChatBox.refreshBorder(); + try { + client.sendMessage(new ChatHistoryRequest(moveToChannel.getChannelId(), ChatHistoryRequest.Source.CHANNEL, "")); + client.sendMessage(new ChannelUsersRequest(moveToChannel.getChannelId())); + } catch (IOException e) { + e.printStackTrace(); + } + this.guiThread.invokeLater(() -> { + this.channelList.setChannels(); + this.channelChatBox.getChatList().clearItems(); + this.channelChatBox.refreshBorder(); + this.channelChatBox.getInputTextBox().takeFocus(); + }); + } else if (message instanceof ChannelUsersResponse channelUsersResponse) { + this.guiThread.invokeLater(() -> { + this.userList.updateUsers(channelUsersResponse); + }); + } else if (message instanceof ChatHistoryResponse chatHistoryResponse) { + System.out.println("Got chat history response: " + chatHistoryResponse.getSourceId()); + System.out.println(chatHistoryResponse.getMessages()); } } } diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/UserList.java b/client/src/main/java/nl/andrewl/concord_client/gui/UserList.java index a6c1979..be92b30 100644 --- a/client/src/main/java/nl/andrewl/concord_client/gui/UserList.java +++ b/client/src/main/java/nl/andrewl/concord_client/gui/UserList.java @@ -1,6 +1,27 @@ package nl.andrewl.concord_client.gui; -import com.googlecode.lanterna.gui2.AbstractListBox; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import nl.andrewl.concord_client.ConcordClient; +import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; -public class UserList extends AbstractListBox { +public class UserList extends Panel { + private final ConcordClient client; + + public UserList(ConcordClient client) { + super(new LinearLayout(Direction.VERTICAL)); + this.client = client; + } + + public void updateUsers(ChannelUsersResponse usersResponse) { + this.removeAllComponents(); + for (var user : usersResponse.getUsers()) { + Button b = new Button(user.getName(), () -> { + System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId()); + }); + this.addComponent(b); + } + } } diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java index 50827b5..090f12c 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java @@ -19,6 +19,8 @@ public class Serializer { registerType(3, MoveToChannel.class); registerType(4, ChatHistoryRequest.class); registerType(5, ChatHistoryResponse.class); + registerType(6, ChannelUsersRequest.class); + registerType(7, ChannelUsersResponse.class); } private static void registerType(int id, Class clazz) { diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java new file mode 100644 index 0000000..b33e598 --- /dev/null +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java @@ -0,0 +1,35 @@ +package nl.andrewl.concord_core.msg.types; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import nl.andrewl.concord_core.msg.Message; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.UUID; + +import static nl.andrewl.concord_core.msg.MessageUtils.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChannelUsersRequest implements Message { + private UUID channelId; + + @Override + public int getByteCount() { + return UUID_BYTES; + } + + @Override + public void write(DataOutputStream o) throws IOException { + writeUUID(this.channelId, o); + } + + @Override + public void read(DataInputStream i) throws IOException { + this.channelId = readUUID(i); + } +} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java new file mode 100644 index 0000000..c91f7f0 --- /dev/null +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java @@ -0,0 +1,65 @@ +package nl.andrewl.concord_core.msg.types; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import nl.andrewl.concord_core.msg.Message; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import static nl.andrewl.concord_core.msg.MessageUtils.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChannelUsersResponse implements Message { + private List users; + + @Override + public int getByteCount() { + return getByteSize(this.users); + } + + @Override + public void write(DataOutputStream o) throws IOException { + writeList(this.users, o); + } + + @Override + public void read(DataInputStream i) throws IOException { + try { + this.users = readList(UserData.class, i); + } catch (ReflectiveOperationException e) { + throw new IOException(e); + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class UserData implements Message { + private UUID id; + private String name; + + @Override + public int getByteCount() { + return UUID_BYTES + getByteSize(this.name); + } + + @Override + public void write(DataOutputStream o) throws IOException { + writeUUID(this.id, o); + writeString(this.name, o); + } + + @Override + public void read(DataInputStream i) throws IOException { + this.id = readUUID(i); + this.name = readString(i); + } + } +} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java index a1d3de2..07a8894 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java @@ -1,5 +1,6 @@ package nl.andrewl.concord_core.msg.types; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import nl.andrewl.concord_core.msg.Message; @@ -46,6 +47,7 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*; */ @Data @NoArgsConstructor +@AllArgsConstructor public class ChatHistoryRequest implements Message { public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java index 2f25a0c..7f74e12 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java @@ -1,5 +1,6 @@ package nl.andrewl.concord_core.msg.types; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import nl.andrewl.concord_core.msg.Message; @@ -17,6 +18,7 @@ import java.util.UUID; */ @Data @NoArgsConstructor +@AllArgsConstructor public class ChatHistoryResponse implements Message { private UUID sourceId; private ChatHistoryRequest.Source sourceType; diff --git a/server/pom.xml b/server/pom.xml index 5b86686..92da267 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -24,5 +24,24 @@ 3.4.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.4 + + + + com.fasterxml.jackson.core + jackson-core + 2.12.4 + + + + com.fasterxml.jackson.core + jackson-annotations + 2.12.4 + + \ No newline at end of file diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 71dd92c..9e45cf7 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -1,9 +1,14 @@ module concord_server { requires nitrite; requires static lombok; + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; requires java.base; requires java.logging; requires concord_core; + + opens nl.andrewl.concord_server.config to com.fasterxml.jackson.databind; } \ No newline at end of file diff --git a/server/src/main/java/nl/andrewl/concord_server/Channel.java b/server/src/main/java/nl/andrewl/concord_server/Channel.java index a79a9d3..642c5bd 100644 --- a/server/src/main/java/nl/andrewl/concord_server/Channel.java +++ b/server/src/main/java/nl/andrewl/concord_server/Channel.java @@ -3,12 +3,12 @@ package nl.andrewl.concord_server; import lombok.Getter; import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_core.msg.Serializer; +import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; +import org.dizitart.no2.NitriteCollection; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -23,19 +23,32 @@ public class Channel { private final Set connectedClients; - public Channel(ConcordServer server, UUID id, String name) { + private final NitriteCollection messageCollection; + + public Channel(ConcordServer server, UUID id, String name, NitriteCollection messageCollection) { this.server = server; this.id = id; this.name = name; this.connectedClients = ConcurrentHashMap.newKeySet(); + this.messageCollection = messageCollection; } public void addClient(ClientThread clientThread) { this.connectedClients.add(clientThread); + try { + this.sendMessage(new ChannelUsersResponse(this.getUserData())); + } catch (IOException e) { + e.printStackTrace(); + } } public void removeClient(ClientThread clientThread) { this.connectedClients.remove(clientThread); + try { + this.sendMessage(new ChannelUsersResponse(this.getUserData())); + } catch (IOException e) { + e.printStackTrace(); + } } /** @@ -53,6 +66,15 @@ public class Channel { } } + public List getUserData() { + List users = new ArrayList<>(); + for (var clientThread : this.getConnectedClients()) { + users.add(new ChannelUsersResponse.UserData(clientThread.getClientId(), clientThread.getClientNickname())); + } + users.sort(Comparator.comparing(ChannelUsersResponse.UserData::getName)); + return users; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java b/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java index 4e247c0..c38d20e 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java +++ b/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java @@ -2,7 +2,10 @@ package nl.andrewl.concord_server; import nl.andrewl.concord_core.msg.types.MoveToChannel; -import java.util.*; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class ChannelManager { @@ -14,10 +17,6 @@ public class ChannelManager { this.server = server; this.channelNameMap = new ConcurrentHashMap<>(); this.channelIdMap = new ConcurrentHashMap<>(); - Channel general = new Channel(server, server.getIdProvider().newId(), "general"); - Channel memes = new Channel(server, server.getIdProvider().newId(), "memes"); - this.addChannel(general); - this.addChannel(memes); } public Set getChannels() { @@ -44,7 +43,8 @@ public class ChannelManager { public void moveToChannel(ClientThread client, Channel channel) { if (client.getCurrentChannel() != null) { - client.getCurrentChannel().removeClient(client); + var previousChannel = client.getCurrentChannel(); + previousChannel.removeClient(client); } channel.addClient(client); client.setCurrentChannel(channel); diff --git a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java index 988d17c..dea8a95 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java +++ b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java @@ -6,7 +6,6 @@ import lombok.extern.java.Log; import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_core.msg.Serializer; import nl.andrewl.concord_core.msg.types.Identification; -import nl.andrewl.concord_core.msg.types.ServerWelcome; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -16,7 +15,7 @@ import java.util.UUID; /** * This thread is responsible for handling the connection to a single client of - * a server. + * a server. The client thread acts as the server's representation of a client. */ @Log public class ClientThread extends Thread { @@ -26,8 +25,11 @@ public class ClientThread extends Thread { private final ConcordServer server; + @Getter + @Setter private UUID clientId = null; @Getter + @Setter private String clientNickname = null; @Getter @@ -43,7 +45,7 @@ public class ClientThread extends Thread { this.out = new DataOutputStream(socket.getOutputStream()); } - public void sendToClient(Message message) { + public synchronized void sendToClient(Message message) { try { Serializer.writeMessage(message, this.out); } catch (IOException e) { @@ -51,7 +53,7 @@ public class ClientThread extends Thread { } } - public void sendToClient(byte[] bytes) { + public synchronized void sendToClient(byte[] bytes) { try { this.out.write(bytes); this.out.flush(); @@ -112,8 +114,7 @@ public class ClientThread extends Thread { try { var msg = Serializer.readMessage(this.in); if (msg instanceof Identification id) { - this.clientNickname = id.getNickname(); - this.clientId = this.server.registerClient(id, this); + this.server.registerClient(id, this); return true; } } catch (IOException e) { diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java index 13c13b6..5e531b3 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java +++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java @@ -5,11 +5,15 @@ import lombok.extern.java.Log; import nl.andrewl.concord_core.msg.types.Identification; import nl.andrewl.concord_core.msg.types.ServerMetaData; import nl.andrewl.concord_core.msg.types.ServerWelcome; +import nl.andrewl.concord_server.config.ServerConfig; +import org.dizitart.no2.IndexOptions; +import org.dizitart.no2.IndexType; import org.dizitart.no2.Nitrite; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +import java.nio.file.Path; import java.util.Comparator; import java.util.Map; import java.util.UUID; @@ -22,6 +26,7 @@ import java.util.stream.Collectors; public class ConcordServer implements Runnable { private final Map clients; private final int port; + private final String name; @Getter private final IdProvider idProvider; @Getter @@ -34,9 +39,11 @@ public class ConcordServer implements Runnable { @Getter private final ChannelManager channelManager; - public ConcordServer(int port) { - this.port = port; + public ConcordServer() { this.idProvider = new UUIDProvider(); + ServerConfig config = ServerConfig.loadOrCreate(Path.of("server-config.json"), idProvider); + this.port = config.port(); + this.name = config.name(); this.db = Nitrite.builder() .filePath("concord-server.db") .openOrCreate(); @@ -45,6 +52,33 @@ public class ConcordServer implements Runnable { this.executorService = Executors.newCachedThreadPool(); this.eventManager = new EventManager(this); this.channelManager = new ChannelManager(this); + for (var channelConfig : config.channels()) { + this.channelManager.addChannel(new Channel( + this, + UUID.fromString(channelConfig.id()), + channelConfig.name(), + this.db.getCollection("channel-" + channelConfig.id()) + )); + } + this.initDatabase(); + } + + private void initDatabase() { + for (var channel : this.channelManager.getChannels()) { + var col = channel.getMessageCollection(); + if (!col.hasIndex("timestamp")) { + log.info("Adding timestamp index to collection for channel " + channel.getName()); + col.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique)); + } + if (!col.hasIndex("senderNickname")) { + log.info("Adding senderNickname index to collection for channel " + channel.getName()); + col.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext)); + } + if (!col.hasIndex("message")) { + log.info("Adding message index to collection for channel " + channel.getName()); + col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext)); + } + } } /** @@ -55,28 +89,34 @@ public class ConcordServer implements Runnable { * channel. * @param identification The client's identification data. * @param clientThread The client manager thread. - * @return The id of the client. */ - public UUID registerClient(Identification identification, ClientThread clientThread) { + public void registerClient(Identification identification, ClientThread clientThread) { var id = this.idProvider.newId(); log.info("Registering new client " + identification.getNickname() + " with id " + id); this.clients.put(id, clientThread); + clientThread.setClientId(id); + clientThread.setClientNickname(identification.getNickname()); // Send a welcome reply containing all the initial server info the client needs. ServerMetaData metaData = new ServerMetaData( - "Testing Server", + this.name, this.channelManager.getChannels().stream() .map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName())) .sorted(Comparator.comparing(ServerMetaData.ChannelData::getName)) .collect(Collectors.toList()) ); + // Immediately add the client to the default channel and send the initial welcome message. var defaultChannel = this.channelManager.getChannelByName("general").orElseThrow(); + clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData)); + // It is important that we send the welcome message first. The client expects this as the initial response to their identification message. defaultChannel.addClient(clientThread); clientThread.setCurrentChannel(defaultChannel); - clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData)); - - return id; } + /** + * De-registers a client from the server, removing them from any channel + * they're currently in. + * @param clientId The id of the client to remove. + */ public void deregisterClient(UUID clientId) { var client = this.clients.remove(clientId); if (client != null) { @@ -104,7 +144,7 @@ public class ConcordServer implements Runnable { } public static void main(String[] args) { - var server = new ConcordServer(8123); + var server = new ConcordServer(); server.run(); } } diff --git a/server/src/main/java/nl/andrewl/concord_server/EventManager.java b/server/src/main/java/nl/andrewl/concord_server/EventManager.java index cdc1deb..2389a4d 100644 --- a/server/src/main/java/nl/andrewl/concord_server/EventManager.java +++ b/server/src/main/java/nl/andrewl/concord_server/EventManager.java @@ -2,15 +2,19 @@ package nl.andrewl.concord_server; import lombok.extern.java.Log; import nl.andrewl.concord_core.msg.Message; +import nl.andrewl.concord_core.msg.types.ChannelUsersRequest; import nl.andrewl.concord_core.msg.types.Chat; +import nl.andrewl.concord_core.msg.types.ChatHistoryRequest; import nl.andrewl.concord_core.msg.types.MoveToChannel; -import nl.andrewl.concord_server.event.ChannelMoveHandler; -import nl.andrewl.concord_server.event.ChatHandler; -import nl.andrewl.concord_server.event.MessageHandler; +import nl.andrewl.concord_server.event.*; import java.util.HashMap; import java.util.Map; +/** + * The event manager is responsible for the server's ability to respond to + * various client requests. + */ @Log public class EventManager { private final Map, MessageHandler> messageHandlers; @@ -21,8 +25,25 @@ public class EventManager { this.messageHandlers = new HashMap<>(); this.messageHandlers.put(Chat.class, new ChatHandler()); this.messageHandlers.put(MoveToChannel.class, new ChannelMoveHandler()); + this.messageHandlers.put(ChannelUsersRequest.class, new ChannelUsersRequestHandler()); + this.messageHandlers.put(ChatHistoryRequest.class, new ChatHistoryRequestHandler()); } + /** + * Handles a new message that was sent from a client. Tries to find an + * appropriate handler for the message, and if one is found, calls the + * {@link MessageHandler#handle(Message, ClientThread, ConcordServer)} + * method on it. + *

+ * Note that it is expected that client threads will invoke this method + * during their {@link ClientThread#run()} method, so concurrent + * invocation is expected. + *

+ * @param message The message that was sent by a client. + * @param client The client thread that is used for communicating with the + * client. + * @param The type of message. + */ @SuppressWarnings("unchecked") public void handle(T message, ClientThread client) { MessageHandler handler = (MessageHandler) this.messageHandlers.get(message.getClass()); @@ -30,6 +51,7 @@ public class EventManager { try { handler.handle(message, client, this.server); } catch (Exception e) { + e.printStackTrace(); log.warning("Exception occurred while handling message: " + e.getMessage()); } } diff --git a/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java b/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java new file mode 100644 index 0000000..b98fc3a --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java @@ -0,0 +1,52 @@ +package nl.andrewl.concord_server.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.java.Log; +import nl.andrewl.concord_server.IdProvider; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +@Log +public record ServerConfig( + String name, + int port, + ChannelConfig[] channels +) { + + public static record ChannelConfig ( + String id, + String name, + String description + ) {} + + public static ServerConfig loadOrCreate(Path filePath, IdProvider idProvider) { + ObjectMapper mapper = new ObjectMapper(); + ServerConfig config; + if (Files.notExists(filePath)) { + config = new ServerConfig( + "My Concord Server", + 8123, + new ServerConfig.ChannelConfig[]{ + new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.") + } + ); + try (var out = Files.newOutputStream(filePath)) { + mapper.writerWithDefaultPrettyPrinter().writeValue(out, config); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + log.info(filePath + " does not exist. Creating it with initial values. Edit and restart to apply changes."); + } else { + try { + config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + log.info("Loaded configuration from " + filePath); + } + return config; + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java new file mode 100644 index 0000000..df0d72b --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java @@ -0,0 +1,17 @@ +package nl.andrewl.concord_server.event; + +import nl.andrewl.concord_core.msg.types.ChannelUsersRequest; +import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; +import nl.andrewl.concord_server.ClientThread; +import nl.andrewl.concord_server.ConcordServer; + +public class ChannelUsersRequestHandler implements MessageHandler { + @Override + public void handle(ChannelUsersRequest msg, ClientThread client, ConcordServer server) throws Exception { + var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId()); + if (optionalChannel.isPresent()) { + var channel = optionalChannel.get(); + client.sendToClient(new ChannelUsersResponse(channel.getUserData())); + } + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java index 7f395ad..bae8f1e 100644 --- a/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java +++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java @@ -12,10 +12,8 @@ public class ChatHandler implements MessageHandler { @Override public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException { server.getExecutorService().submit(() -> { - var collection = server.getDb().getCollection("channel-" + client.getCurrentChannel().getId()); - var messageId = server.getIdProvider().newId(); + var collection = client.getCurrentChannel().getMessageCollection(); Document doc = new Document(Map.of( - "_id", messageId, "senderId", msg.getSenderId(), "senderNickname", msg.getSenderNickname(), "timestamp", msg.getTimestamp(), diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java new file mode 100644 index 0000000..8f8fd40 --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java @@ -0,0 +1,42 @@ +package nl.andrewl.concord_server.event; + +import nl.andrewl.concord_core.msg.types.Chat; +import nl.andrewl.concord_core.msg.types.ChatHistoryRequest; +import nl.andrewl.concord_core.msg.types.ChatHistoryResponse; +import nl.andrewl.concord_server.ClientThread; +import nl.andrewl.concord_server.ConcordServer; +import org.dizitart.no2.Document; +import org.dizitart.no2.FindOptions; +import org.dizitart.no2.SortOrder; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ChatHistoryRequestHandler implements MessageHandler { + @Override + public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) throws Exception { + var optionalChannel = server.getChannelManager().getChannelById(msg.getSourceId()); + if (optionalChannel.isPresent()) { + var channel = optionalChannel.get(); + System.out.println("Looking for chats in channel-" + channel.getId()); + var col = server.getDb().getCollection("channel-" + channel.getId()); + var cursor = col.find( + FindOptions.sort("timestamp", SortOrder.Ascending) + .thenLimit(0, 10) + ); + List chats = new ArrayList<>(10); + for (Document doc : cursor) { + chats.add(new Chat( + doc.get("senderId", UUID.class), + doc.get("senderNickname", String.class), + doc.get("timestamp", Long.class), + doc.get("message", String.class) + )); + } + col.close(); + System.out.println(chats); + client.sendToClient(new ChatHistoryResponse(msg.getSourceId(), msg.getSourceType(), chats)); + } + } +}