From f159708fa26875532c9cdec48ae5de623bb2fd27 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 25 Aug 2021 19:04:26 +0200 Subject: [PATCH] Added ability to add and remove channels. --- .../andrewl/concord_client/ConcordClient.java | 2 + .../event/ClientModelListener.java | 7 +- .../event/handlers/ServerMetaDataHandler.java | 12 +++ .../concord_client/gui/ServerPanel.java | 17 ++-- .../andrewl/concord_client/gui/UserList.java | 3 +- .../concord_client/model/ClientModel.java | 11 ++- .../andrewl/concord_core/msg/Serializer.java | 1 + .../msg/types/ChannelUsersResponse.java | 26 ------ .../concord_core/msg/types/UserData.java | 43 +++++++++ .../nl/andrewl/concord_server/Channel.java | 34 ++++++- .../andrewl/concord_server/ClientThread.java | 5 + .../andrewl/concord_server/ConcordServer.java | 91 +++++++++++-------- .../andrewl/concord_server/cli/ServerCli.java | 53 +++++++++++ .../concord_server/cli/ServerCliCommand.java | 7 ++ .../cli/command/AddChannelCommand.java | 36 ++++++++ .../cli/command/ListClientsCommand.java | 20 ++++ .../cli/command/RemoveChannelCommand.java | 41 +++++++++ .../concord_server/config/ServerConfig.java | 57 ++++++++---- .../event/ChatHistoryRequestHandler.java | 4 +- 19 files changed, 370 insertions(+), 100 deletions(-) create mode 100644 client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerMetaDataHandler.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java create mode 100644 server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java create mode 100644 server/src/main/java/nl/andrewl/concord_server/cli/ServerCliCommand.java create mode 100644 server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java create mode 100644 server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java create mode 100644 server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java 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 cdbe102..9e85986 100644 --- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java +++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java @@ -12,6 +12,7 @@ import nl.andrewl.concord_client.event.EventManager; import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler; import nl.andrewl.concord_client.event.handlers.ChannelUsersResponseHandler; import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler; +import nl.andrewl.concord_client.event.handlers.ServerMetaDataHandler; import nl.andrewl.concord_client.gui.MainWindow; import nl.andrewl.concord_client.model.ClientModel; import nl.andrewl.concord_core.msg.Message; @@ -57,6 +58,7 @@ public class ConcordClient implements Runnable { this.eventManager.addHandler(ChannelUsersResponse.class, new ChannelUsersResponseHandler()); this.eventManager.addHandler(ChatHistoryResponse.class, new ChatHistoryResponseHandler()); this.eventManager.addHandler(Chat.class, (msg, client) -> client.getModel().getChatHistory().addChat(msg)); + this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler()); } public void sendMessage(Message message) throws IOException { diff --git a/client/src/main/java/nl/andrewl/concord_client/event/ClientModelListener.java b/client/src/main/java/nl/andrewl/concord_client/event/ClientModelListener.java index 469ff35..d74232e 100644 --- a/client/src/main/java/nl/andrewl/concord_client/event/ClientModelListener.java +++ b/client/src/main/java/nl/andrewl/concord_client/event/ClientModelListener.java @@ -1,6 +1,7 @@ package nl.andrewl.concord_client.event; -import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; +import nl.andrewl.concord_core.msg.types.ServerMetaData; +import nl.andrewl.concord_core.msg.types.UserData; import java.util.List; import java.util.UUID; @@ -8,5 +9,7 @@ import java.util.UUID; public interface ClientModelListener { default void channelMoved(UUID oldChannelId, UUID newChannelId) {} - default void usersUpdated(List users) {} + default void usersUpdated(List users) {} + + default void serverMetaDataUpdated(ServerMetaData metaData) {} } diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerMetaDataHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerMetaDataHandler.java new file mode 100644 index 0000000..05dabd8 --- /dev/null +++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerMetaDataHandler.java @@ -0,0 +1,12 @@ +package nl.andrewl.concord_client.event.handlers; + +import nl.andrewl.concord_client.ConcordClient; +import nl.andrewl.concord_client.event.MessageHandler; +import nl.andrewl.concord_core.msg.types.ServerMetaData; + +public class ServerMetaDataHandler implements MessageHandler { + @Override + public void handle(ServerMetaData msg, ConcordClient client) { + client.getModel().setServerMetaData(msg); + } +} diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ServerPanel.java b/client/src/main/java/nl/andrewl/concord_client/gui/ServerPanel.java index 9c57e44..911cc7b 100644 --- a/client/src/main/java/nl/andrewl/concord_client/gui/ServerPanel.java +++ b/client/src/main/java/nl/andrewl/concord_client/gui/ServerPanel.java @@ -4,7 +4,8 @@ import com.googlecode.lanterna.gui2.*; import lombok.Getter; import nl.andrewl.concord_client.ConcordClient; import nl.andrewl.concord_client.event.ClientModelListener; -import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; +import nl.andrewl.concord_core.msg.types.ServerMetaData; +import nl.andrewl.concord_core.msg.types.UserData; import java.util.List; import java.util.UUID; @@ -22,11 +23,8 @@ public class ServerPanel extends Panel implements ClientModelListener { private final ChannelList channelList; private final UserList userList; - private final TextGUIThread guiThread; - public ServerPanel(ConcordClient client, Window window) { super(new BorderLayout()); - this.guiThread = window.getTextGUI().getGUIThread(); this.channelChatBox = new ChannelChatBox(client, window); this.channelList = new ChannelList(client); this.channelList.setChannels(); @@ -55,9 +53,16 @@ public class ServerPanel extends Panel implements ClientModelListener { } @Override - public void usersUpdated(List users) { - this.guiThread.invokeLater(() -> { + public void usersUpdated(List users) { + this.getTextGUI().getGUIThread().invokeLater(() -> { this.userList.updateUsers(users); }); } + + @Override + public void serverMetaDataUpdated(ServerMetaData metaData) { + this.getTextGUI().getGUIThread().invokeLater(() -> { + this.channelList.setChannels(); + }); + } } 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 ef99a1b..93cc91a 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 @@ -6,6 +6,7 @@ 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; +import nl.andrewl.concord_core.msg.types.UserData; import java.util.List; @@ -17,7 +18,7 @@ public class UserList extends Panel { this.client = client; } - public void updateUsers(List usersResponse) { + public void updateUsers(List usersResponse) { this.removeAllComponents(); for (var user : usersResponse) { Button b = new Button(user.getName(), () -> { diff --git a/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java b/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java index 3a44273..02a52b2 100644 --- a/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java +++ b/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java @@ -2,8 +2,8 @@ package nl.andrewl.concord_client.model; import lombok.Getter; import nl.andrewl.concord_client.event.ClientModelListener; -import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; import nl.andrewl.concord_core.msg.types.ServerMetaData; +import nl.andrewl.concord_core.msg.types.UserData; import java.util.ArrayList; import java.util.List; @@ -17,7 +17,7 @@ public class ClientModel { private ServerMetaData serverMetaData; private UUID currentChannelId; - private List knownUsers; + private List knownUsers; private final ChatHistory chatHistory; private final List modelListeners; @@ -38,11 +38,16 @@ public class ClientModel { this.modelListeners.forEach(listener -> listener.channelMoved(oldId, newChannelId)); } - public void setKnownUsers(List users) { + public void setKnownUsers(List users) { this.knownUsers = users; this.modelListeners.forEach(listener -> listener.usersUpdated(this.knownUsers)); } + public void setServerMetaData(ServerMetaData metaData) { + this.serverMetaData = metaData; + this.modelListeners.forEach(listener -> listener.serverMetaDataUpdated(metaData)); + } + public void addListener(ClientModelListener listener) { this.modelListeners.add(listener); } 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 090f12c..11c1f93 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 @@ -21,6 +21,7 @@ public class Serializer { registerType(5, ChatHistoryResponse.class); registerType(6, ChannelUsersRequest.class); registerType(7, ChannelUsersResponse.class); + registerType(8, ServerMetaData.class); } private static void registerType(int id, Class clazz) { 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 index c91f7f0..c9c6d28 100644 --- 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 @@ -9,7 +9,6 @@ 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.*; @@ -37,29 +36,4 @@ public class ChannelUsersResponse implements Message { 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/UserData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java new file mode 100644 index 0000000..f050a2b --- /dev/null +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java @@ -0,0 +1,43 @@ +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.*; +import static nl.andrewl.concord_core.msg.MessageUtils.readString; + +/** + * Standard set of user data that is used mainly as a component of other more + * complex messages. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public 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/server/src/main/java/nl/andrewl/concord_server/Channel.java b/server/src/main/java/nl/andrewl/concord_server/Channel.java index 96eecfc..18eb515 100644 --- a/server/src/main/java/nl/andrewl/concord_server/Channel.java +++ b/server/src/main/java/nl/andrewl/concord_server/Channel.java @@ -4,6 +4,9 @@ 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 nl.andrewl.concord_core.msg.types.UserData; +import org.dizitart.no2.IndexOptions; +import org.dizitart.no2.IndexType; import org.dizitart.no2.NitriteCollection; import java.io.ByteArrayOutputStream; @@ -31,6 +34,29 @@ public class Channel { this.name = name; this.connectedClients = ConcurrentHashMap.newKeySet(); this.messageCollection = messageCollection; + this.initCollection(); + } + + private void initCollection() { + if (!this.messageCollection.hasIndex("timestamp")) { + System.out.println("Adding index on \"timestamp\" field to collection " + this.messageCollection.getName()); + this.messageCollection.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique)); + } + if (!this.messageCollection.hasIndex("senderNickname")) { + System.out.println("Adding index on \"senderNickname\" field to collection " + this.messageCollection.getName()); + this.messageCollection.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext)); + } + if (!this.messageCollection.hasIndex("message")) { + System.out.println("Adding index on \"message\" field to collection " + this.messageCollection.getName()); + this.messageCollection.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext)); + } + var fields = List.of("timestamp", "senderNickname", "message"); + for (var index : this.messageCollection.listIndices()) { + if (!fields.contains(index.getField())) { + System.out.println("Dropping unknown index " + index.getField() + " from collection " + index.getCollectionName()); + this.messageCollection.dropIndex(index.getField()); + } + } } public void addClient(ClientThread clientThread) { @@ -66,12 +92,12 @@ public class Channel { } } - public List getUserData() { - List users = new ArrayList<>(); + public List getUserData() { + List users = new ArrayList<>(this.connectedClients.size()); for (var clientThread : this.getConnectedClients()) { - users.add(new ChannelUsersResponse.UserData(clientThread.getClientId(), clientThread.getClientNickname())); + users.add(clientThread.toData()); } - users.sort(Comparator.comparing(ChannelUsersResponse.UserData::getName)); + users.sort(Comparator.comparing(UserData::getName)); return users; } 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 19f7595..10a4339 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java +++ b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java @@ -5,6 +5,7 @@ import lombok.Setter; 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.UserData; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -122,6 +123,10 @@ public class ClientThread extends Thread { return false; } + public UserData toData() { + return new UserData(this.clientId, this.clientNickname); + } + @Override public String toString() { return this.clientNickname + " (" + this.clientId + ")"; 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 3958e38..0ed6e67 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java +++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java @@ -1,19 +1,24 @@ 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.Identification; import nl.andrewl.concord_core.msg.types.ServerMetaData; import nl.andrewl.concord_core.msg.types.ServerWelcome; +import nl.andrewl.concord_core.msg.types.UserData; +import nl.andrewl.concord_server.cli.ServerCli; 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.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.nio.file.Path; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -52,33 +57,14 @@ public class ConcordServer implements Runnable { this.executorService = Executors.newCachedThreadPool(); this.eventManager = new EventManager(this); this.channelManager = new ChannelManager(this); - for (var channelConfig : config.channels()) { + for (var channelConfig : config.getChannels()) { this.channelManager.addChannel(new Channel( this, - UUID.fromString(channelConfig.id()), - channelConfig.name(), - this.db.getCollection("channel-" + channelConfig.id()) + UUID.fromString(channelConfig.getId()), + channelConfig.getName(), + this.db.getCollection("channel-" + channelConfig.getId()) )); } - this.updateDatabase(); - } - - private void updateDatabase() { - for (var channel : this.channelManager.getChannels()) { - var col = channel.getMessageCollection(); - if (!col.hasIndex("timestamp")) { - System.out.println("Adding timestamp index to collection for channel " + channel.getName()); - col.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique)); - } - if (!col.hasIndex("senderNickname")) { - System.out.println("Adding senderNickname index to collection for channel " + channel.getName()); - col.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext)); - } - if (!col.hasIndex("message")) { - System.out.println("Adding message index to collection for channel " + channel.getName()); - col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext)); - } - } } /** @@ -96,17 +82,9 @@ public class ConcordServer implements Runnable { 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( - this.config.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)); + clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), this.getMetaData())); // 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); @@ -127,14 +105,52 @@ public class ConcordServer implements Runnable { } } + public boolean isRunning() { + return running; + } + + public List getClients() { + return this.clients.values().stream() + .sorted(Comparator.comparing(ClientThread::getClientNickname)) + .map(ClientThread::toData) + .collect(Collectors.toList()); + } + + public ServerMetaData getMetaData() { + return new ServerMetaData( + this.config.getName(), + this.channelManager.getChannels().stream() + .map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName())) + .sorted(Comparator.comparing(ServerMetaData.ChannelData::getName)) + .collect(Collectors.toList()) + ); + } + + /** + * Sends a message to every connected client. + * @param message The message to send. + */ + public void broadcast(Message message) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount()); + try { + Serializer.writeMessage(message, baos); + byte[] data = baos.toByteArray(); + for (var client : this.clients.values()) { + client.sendToClient(data); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + @Override public void run() { this.running = true; ServerSocket serverSocket; try { - serverSocket = new ServerSocket(this.config.port()); + serverSocket = new ServerSocket(this.config.getPort()); StringBuilder startupMessage = new StringBuilder(); - startupMessage.append("Opened server on port ").append(config.port()).append("\n"); + startupMessage.append("Opened server on port ").append(config.getPort()).append("\n"); for (var channel : this.channelManager.getChannels()) { startupMessage.append("\tChannel \"").append(channel).append('\n'); } @@ -151,6 +167,7 @@ public class ConcordServer implements Runnable { public static void main(String[] args) { var server = new ConcordServer(); - server.run(); + new Thread(server).start(); + new ServerCli(server).run(); } } diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java new file mode 100644 index 0000000..80f4e8a --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java @@ -0,0 +1,53 @@ +package nl.andrewl.concord_server.cli; + +import nl.andrewl.concord_server.ConcordServer; +import nl.andrewl.concord_server.cli.command.AddChannelCommand; +import nl.andrewl.concord_server.cli.command.ListClientsCommand; +import nl.andrewl.concord_server.cli.command.RemoveChannelCommand; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class ServerCli implements Runnable { + private final ConcordServer server; + private final Map commands; + + public ServerCli(ConcordServer server) { + this.server = server; + this.commands = new HashMap<>(); + + this.commands.put("list-clients", new ListClientsCommand()); + this.commands.put("add-channel", new AddChannelCommand()); + this.commands.put("remove-channel", new RemoveChannelCommand()); + } + + @Override + public void run() { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String line; + try { + while (this.server.isRunning() && (line = reader.readLine()) != null) { + if (!line.isBlank()) { + String[] words = line.split("\\s+"); + String command = words[0]; + String[] args = Arrays.copyOfRange(words, 1, words.length); + var cliCommand = this.commands.get(command.trim().toLowerCase()); + if (cliCommand != null) { + try { + cliCommand.handle(this.server, args); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + System.err.println("Unknown command."); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/ServerCliCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCliCommand.java new file mode 100644 index 0000000..ed447d0 --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCliCommand.java @@ -0,0 +1,7 @@ +package nl.andrewl.concord_server.cli; + +import nl.andrewl.concord_server.ConcordServer; + +public interface ServerCliCommand { + void handle(ConcordServer server, String[] args) throws Exception; +} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java new file mode 100644 index 0000000..5619eac --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java @@ -0,0 +1,36 @@ +package nl.andrewl.concord_server.cli.command; + +import nl.andrewl.concord_server.Channel; +import nl.andrewl.concord_server.ConcordServer; +import nl.andrewl.concord_server.cli.ServerCliCommand; +import nl.andrewl.concord_server.config.ServerConfig; + +import java.util.UUID; + +public class AddChannelCommand implements ServerCliCommand { + @Override + public void handle(ConcordServer server, String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Missing required name argument."); + } + String name = args[0].trim().toLowerCase().replaceAll("\\s+", "-"); + if (name.isBlank()) { + System.err.println("Cannot create channel with blank name."); + } + if (server.getChannelManager().getChannelByName(name).isPresent()) { + System.err.println("Channel with that name already exists."); + } + String description = null; + if (args.length > 1) { + description = args[1].trim(); + } + UUID id = server.getIdProvider().newId(); + var channelConfig = new ServerConfig.ChannelConfig(id.toString(), name, description); + server.getConfig().getChannels().add(channelConfig); + server.getConfig().save(); + + var col = server.getDb().getCollection("channel-" + id); + server.getChannelManager().addChannel(new Channel(server, id, name, col)); + server.broadcast(server.getMetaData()); + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java new file mode 100644 index 0000000..3501084 --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java @@ -0,0 +1,20 @@ +package nl.andrewl.concord_server.cli.command; + +import nl.andrewl.concord_server.ConcordServer; +import nl.andrewl.concord_server.cli.ServerCliCommand; + +public class ListClientsCommand implements ServerCliCommand { + @Override + public void handle(ConcordServer server, String[] args) throws Exception { + var users = server.getClients(); + if (users.isEmpty()) { + System.out.println("There are no connected clients."); + } else { + StringBuilder sb = new StringBuilder("Online Users:\n"); + for (var userData : users) { + sb.append("\t").append(userData.getName()).append(" (").append(userData.getId()).append(")\n"); + } + System.out.print(sb); + } + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java new file mode 100644 index 0000000..ef90733 --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java @@ -0,0 +1,41 @@ +package nl.andrewl.concord_server.cli.command; + +import nl.andrewl.concord_server.Channel; +import nl.andrewl.concord_server.ConcordServer; +import nl.andrewl.concord_server.cli.ServerCliCommand; + +import java.util.Optional; + +public class RemoveChannelCommand implements ServerCliCommand { + @Override + public void handle(ConcordServer server, String[] args) throws Exception { + if (args.length != 1) { + System.err.println("Missing required channel name."); + return; + } + String name = args[0].trim().toLowerCase(); + Optional optionalChannel = server.getChannelManager().getChannelByName(name); + if (optionalChannel.isEmpty()) { + System.err.println("No channel with that name exists."); + return; + } + Channel channelToRemove = optionalChannel.get(); + Channel alternative = null; + for (var c : server.getChannelManager().getChannels()) { + if (!c.equals(channelToRemove)) { + alternative = c; + break; + } + } + if (alternative == null) { + System.err.println("No alternative channel could be found. A server must always have at least one channel."); + return; + } + for (var client : channelToRemove.getConnectedClients()) { + server.getChannelManager().moveToChannel(client, alternative); + } + server.getChannelManager().removeChannel(channelToRemove); + server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName()); + server.broadcast(server.getMetaData()); + } +} 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 index dd2680a..1a8fc4a 100644 --- a/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java +++ b/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java @@ -1,31 +1,43 @@ package nl.andrewl.concord_server.config; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.java.Log; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import nl.andrewl.concord_server.IdProvider; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; -public record ServerConfig( - String name, - int port, +@Data +@NoArgsConstructor +@AllArgsConstructor +public final class ServerConfig { + private String name; + private int port; + private int chatHistoryMaxCount; + private int chatHistoryDefaultCount; + private int maxMessageLength; + private List channels; - // Global Channel configuration - int chatHistoryMaxCount, - int chatHistoryDefaultCount, - int maxMessageLength, + /** + * The path at which this config is stored. + */ + @JsonIgnore + private transient Path filePath; - ChannelConfig[] channels -) { - - public static record ChannelConfig ( - String id, - String name, - String description - ) {} + @Data + @NoArgsConstructor + @AllArgsConstructor + public static final class ChannelConfig { + private String id; + private String name; + private String description; + } public static ServerConfig loadOrCreate(Path filePath, IdProvider idProvider) { ObjectMapper mapper = new ObjectMapper(); @@ -37,9 +49,8 @@ public record ServerConfig( 100, 50, 8192, - new ServerConfig.ChannelConfig[]{ - new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.") - } + List.of(new ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")), + filePath ); try (var out = Files.newOutputStream(filePath)) { mapper.writerWithDefaultPrettyPrinter().writeValue(out, config); @@ -50,6 +61,7 @@ public record ServerConfig( } else { try { config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class); + config.setFilePath(filePath); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -57,4 +69,11 @@ public record ServerConfig( } return config; } + + public void save() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + try (var out = Files.newOutputStream(filePath)) { + mapper.writerWithDefaultPrettyPrinter().writeValue(out, this); + } + } } 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 index e553ee3..34a4141 100644 --- a/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java +++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java @@ -21,8 +21,8 @@ public class ChatHistoryRequestHandler implements MessageHandler server.getConfig().chatHistoryMaxCount()) { + Long count = this.getOrDefault(params, "count", (long) server.getConfig().getChatHistoryDefaultCount()); + if (count > server.getConfig().getChatHistoryMaxCount()) { return; } Long from = this.getOrDefault(params, "from", null);