diff --git a/client/pom.xml b/client/pom.xml
index 7e85b4d..62de01d 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -21,7 +21,7 @@
com.googlecode.lanterna
lanterna
- 3.1.1
+ 3.2.0-alpha1
\ No newline at end of file
diff --git a/client/src/main/java/nl/andrewl/concord_client/ClientMessageListener.java b/client/src/main/java/nl/andrewl/concord_client/ClientMessageListener.java
deleted file mode 100644
index bac835a..0000000
--- a/client/src/main/java/nl/andrewl/concord_client/ClientMessageListener.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package nl.andrewl.concord_client;
-
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.IOException;
-
-public interface ClientMessageListener {
- void messageReceived(ConcordClient client, Message message) throws IOException;
-}
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 5e5db90..cdbe102 100644
--- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
+++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
@@ -8,8 +8,12 @@ import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;
import lombok.Getter;
-import lombok.Setter;
+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.gui.MainWindow;
+import nl.andrewl.concord_client.model.ClientModel;
import nl.andrewl.concord_core.msg.Message;
import nl.andrewl.concord_core.msg.Serializer;
import nl.andrewl.concord_core.msg.types.*;
@@ -18,52 +22,41 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
-import java.util.HashSet;
import java.util.List;
-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;
- private final UUID id;
- private final String nickname;
+
@Getter
- @Setter
- private UUID currentChannelId;
- @Getter
- private ServerMetaData serverMetaData;
- private final Set messageListeners;
+ private final ClientModel model;
+
+ private final EventManager eventManager;
+
private volatile boolean running;
public ConcordClient(String host, int port, String nickname) throws IOException {
+ this.eventManager = new EventManager(this);
this.socket = new Socket(host, port);
this.in = new DataInputStream(this.socket.getInputStream());
this.out = new DataOutputStream(this.socket.getOutputStream());
- this.nickname = nickname;
Serializer.writeMessage(new Identification(nickname), this.out);
Message reply = Serializer.readMessage(this.in);
if (reply instanceof ServerWelcome welcome) {
- this.id = welcome.getClientId();
- this.currentChannelId = welcome.getCurrentChannelId();
- this.serverMetaData = welcome.getMetaData();
-
+ this.model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), 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, ""));
+ this.sendMessage(new ChannelUsersRequest(this.model.getCurrentChannelId()));
+ this.sendMessage(new ChatHistoryRequest(this.model.getCurrentChannelId(), ""));
} else {
throw new IOException("Unexpected response from the server after sending identification message.");
}
- this.messageListeners = new HashSet<>();
- }
- public void addListener(ClientMessageListener listener) {
- this.messageListeners.add(listener);
- }
-
- public void removeListener(ClientMessageListener listener) {
- this.messageListeners.remove(listener);
+ // Add event listeners.
+ this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
+ 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));
}
public void sendMessage(Message message) throws IOException {
@@ -71,7 +64,7 @@ public class ConcordClient implements Runnable {
}
public void sendChat(String message) throws IOException {
- Serializer.writeMessage(new Chat(this.id, this.nickname, System.currentTimeMillis(), message), this.out);
+ Serializer.writeMessage(new Chat(this.model.getId(), this.model.getNickname(), System.currentTimeMillis(), message), this.out);
}
public void shutdown() {
@@ -91,9 +84,7 @@ public class ConcordClient implements Runnable {
while (this.running) {
try {
Message msg = Serializer.readMessage(this.in);
- for (var listener : this.messageListeners) {
- listener.messageReceived(this, msg);
- }
+ this.eventManager.handle(msg);
} catch (IOException e) {
e.printStackTrace();
this.running = false;
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java b/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java
new file mode 100644
index 0000000..dd78493
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java
@@ -0,0 +1,12 @@
+package nl.andrewl.concord_client.event;
+
+import nl.andrewl.concord_client.model.ChatHistory;
+import nl.andrewl.concord_core.msg.types.Chat;
+
+public interface ChatHistoryListener {
+ default void chatAdded(Chat chat) {}
+
+ default void chatRemoved(Chat chat) {}
+
+ default void chatUpdated(ChatHistory history) {}
+}
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
new file mode 100644
index 0000000..469ff35
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/ClientModelListener.java
@@ -0,0 +1,12 @@
+package nl.andrewl.concord_client.event;
+
+import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface ClientModelListener {
+ default void channelMoved(UUID oldChannelId, UUID newChannelId) {}
+
+ default void usersUpdated(List users) {}
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/EventManager.java b/client/src/main/java/nl/andrewl/concord_client/event/EventManager.java
new file mode 100644
index 0000000..b73caf1
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/EventManager.java
@@ -0,0 +1,38 @@
+package nl.andrewl.concord_client.event;
+
+import nl.andrewl.concord_client.ConcordClient;
+import nl.andrewl.concord_core.msg.Message;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class EventManager {
+ private final Map, List>> messageHandlers;
+ private final ConcordClient client;
+
+ public EventManager(ConcordClient client) {
+ this.client = client;
+ this.messageHandlers = new ConcurrentHashMap<>();
+ }
+
+ public void addHandler(Class messageClass, MessageHandler handler) {
+ var handlers = this.messageHandlers.computeIfAbsent(messageClass, k -> new CopyOnWriteArrayList<>());
+ handlers.add(handler);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void handle(T message) {
+ var handlers = this.messageHandlers.get(message.getClass());
+ if (handlers != null) {
+ for (var handler : handlers) {
+ try {
+ ((MessageHandler) handler).handle(message, this.client);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/MessageHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/MessageHandler.java
new file mode 100644
index 0000000..f3c8fbf
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/MessageHandler.java
@@ -0,0 +1,8 @@
+package nl.andrewl.concord_client.event;
+
+import nl.andrewl.concord_client.ConcordClient;
+import nl.andrewl.concord_core.msg.Message;
+
+public interface MessageHandler {
+ void handle(T msg, ConcordClient client) throws Exception;
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java
new file mode 100644
index 0000000..e59af02
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java
@@ -0,0 +1,16 @@
+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.ChannelUsersRequest;
+import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
+import nl.andrewl.concord_core.msg.types.MoveToChannel;
+
+public class ChannelMovedHandler implements MessageHandler {
+ @Override
+ public void handle(MoveToChannel msg, ConcordClient client) throws Exception {
+ client.getModel().setCurrentChannelId(msg.getChannelId());
+ client.sendMessage(new ChatHistoryRequest(msg.getChannelId(), ""));
+ client.sendMessage(new ChannelUsersRequest(msg.getChannelId()));
+ }
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java
new file mode 100644
index 0000000..ef79234
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.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.ChannelUsersResponse;
+
+public class ChannelUsersResponseHandler implements MessageHandler {
+ @Override
+ public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception {
+ client.getModel().setKnownUsers(msg.getUsers());
+ }
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java
new file mode 100644
index 0000000..77866f5
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.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.ChatHistoryResponse;
+
+public class ChatHistoryResponseHandler implements MessageHandler {
+ @Override
+ public void handle(ChatHistoryResponse msg, ConcordClient client) throws Exception {
+ client.getModel().getChatHistory().setChats(msg.getMessages());
+ }
+}
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 359804c..1ad29ae 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
@@ -26,6 +26,7 @@ public class ChannelChatBox extends Panel {
super(new BorderLayout());
this.client = client;
this.chatList = new ChatList();
+ this.client.getModel().getChatHistory().addListener(this.chatList);
this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
this.inputTextBox.setCaretWarp(true);
this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
@@ -37,7 +38,6 @@ public class ChannelChatBox extends Panel {
String text = inputTextBox.getText();
if (text != null && !text.isBlank()) {
try {
- System.out.println("Sending: " + text.trim());
client.sendChat(text.trim());
inputTextBox.setText("");
} catch (IOException e) {
@@ -53,8 +53,8 @@ public class ChannelChatBox extends Panel {
}
public void refreshBorder() {
- String name = client.getServerMetaData().getChannels().stream()
- .filter(channelData -> channelData.getId().equals(client.getCurrentChannelId()))
+ String name = client.getModel().getServerMetaData().getChannels().stream()
+ .filter(channelData -> channelData.getId().equals(client.getModel().getCurrentChannelId()))
.findAny().orElseThrow().getName();
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
this.chatBorder = Borders.doubleLine("#" + name);
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 aad5eb6..e883888 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
@@ -6,6 +6,10 @@ import nl.andrewl.concord_core.msg.types.MoveToChannel;
import java.io.IOException;
+/**
+ * Panel that contains a list of channels. A user can interact with a channel to
+ * move to that channel. The current channel is indicated via a "*".
+ */
public class ChannelList extends Panel {
private final ConcordClient client;
public ChannelList(ConcordClient client) {
@@ -15,13 +19,13 @@ public class ChannelList extends Panel {
public void setChannels() {
this.removeAllComponents();
- for (var channel : this.client.getServerMetaData().getChannels()) {
+ for (var channel : this.client.getModel().getServerMetaData().getChannels()) {
String name = channel.getName();
- if (client.getCurrentChannelId().equals(channel.getId())) {
+ if (client.getModel().getCurrentChannelId().equals(channel.getId())) {
name = "*" + name;
}
Button b = new Button(name, () -> {
- if (!client.getCurrentChannelId().equals(channel.getId())) {
+ if (!client.getModel().getCurrentChannelId().equals(channel.getId())) {
try {
client.sendMessage(new MoveToChannel(channel.getId()));
} catch (IOException e) {
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
index a02831a..5a5f644 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
@@ -1,9 +1,15 @@
package nl.andrewl.concord_client.gui;
import com.googlecode.lanterna.gui2.AbstractListBox;
+import nl.andrewl.concord_client.event.ChatHistoryListener;
+import nl.andrewl.concord_client.model.ChatHistory;
import nl.andrewl.concord_core.msg.types.Chat;
-public class ChatList extends AbstractListBox {
+/**
+ * This chat list shows a section of chat messages that have been sent in a
+ * single channel (server channel, thread, or direct message).
+ */
+public class ChatList extends AbstractListBox implements ChatHistoryListener {
/**
* Adds one more item to the list box, at the end.
*
@@ -28,4 +34,31 @@ public class ChatList extends AbstractListBox {
protected ListItemRenderer createDefaultListItemRenderer() {
return new ChatRenderer();
}
+
+ @Override
+ public void chatAdded(Chat chat) {
+ this.getTextGUI().getGUIThread().invokeLater(() -> {
+ this.addItem(chat);
+ });
+ }
+
+ @Override
+ public void chatRemoved(Chat chat) {
+ for (int i = 0; i < this.getItemCount(); i++) {
+ if (this.getItemAt(i).equals(chat)) {
+ this.removeItem(i);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void chatUpdated(ChatHistory history) {
+ this.getTextGUI().getGUIThread().invokeLater(() -> {
+ this.clearItems();
+ for (var chat : history.getChats()) {
+ this.addItem(chat);
+ }
+ });
+ }
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java b/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
index 7832f07..71477b6 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
@@ -52,7 +52,7 @@ public class MainWindow extends BasicWindow {
try {
var client = new ConcordClient(host, port, nickname);
var chatPanel = new ServerPanel(client, this);
- client.addListener(chatPanel);
+ client.getModel().addListener(chatPanel);
new Thread(client).start();
this.setComponent(chatPanel);
} catch (IOException e) {
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 3b49dde..9c57e44 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
@@ -2,12 +2,12 @@ package nl.andrewl.concord_client.gui;
import com.googlecode.lanterna.gui2.*;
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.*;
+import nl.andrewl.concord_client.event.ClientModelListener;
+import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
-import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
/**
* The main panel in which a user interacts with the application during normal
@@ -16,19 +16,17 @@ import java.io.IOException;
* meta information in the sidebars which provides the user with a list of all
* threads and users in the server.
*/
-public class ServerPanel extends Panel implements ClientMessageListener {
+public class ServerPanel extends Panel implements ClientModelListener {
@Getter
private final ChannelChatBox channelChatBox;
private final ChannelList channelList;
private final UserList userList;
- private final ConcordClient client;
private final TextGUIThread guiThread;
public ServerPanel(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();
@@ -47,36 +45,19 @@ public class ServerPanel extends Panel implements ClientMessageListener {
}
@Override
- public void messageReceived(ConcordClient client, Message message) {
- if (message instanceof Chat chat) {
- this.channelChatBox.getChatList().addItem(chat);
- } else if (message instanceof MoveToChannel moveToChannel) {
- client.setCurrentChannelId(moveToChannel.getChannelId());
- 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());
- this.guiThread.invokeLater(() -> {
- this.channelChatBox.getChatList().clearItems();
- for (var chat : chatHistoryResponse.getMessages()) {
- this.channelChatBox.getChatList().addItem(chat);
- }
- });
- }
+ public void channelMoved(UUID oldChannelId, UUID newChannelId) {
+ this.getTextGUI().getGUIThread().invokeLater(() -> {
+ this.channelList.setChannels();
+ this.channelChatBox.getChatList().clearItems();
+ this.channelChatBox.refreshBorder();
+ this.channelChatBox.getInputTextBox().takeFocus();
+ });
+ }
+
+ @Override
+ public void usersUpdated(List users) {
+ this.guiThread.invokeLater(() -> {
+ this.userList.updateUsers(users);
+ });
}
}
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 be92b30..ef99a1b 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
@@ -7,6 +7,8 @@ import com.googlecode.lanterna.gui2.Panel;
import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
+import java.util.List;
+
public class UserList extends Panel {
private final ConcordClient client;
@@ -15,9 +17,9 @@ public class UserList extends Panel {
this.client = client;
}
- public void updateUsers(ChannelUsersResponse usersResponse) {
+ public void updateUsers(List usersResponse) {
this.removeAllComponents();
- for (var user : usersResponse.getUsers()) {
+ for (var user : usersResponse) {
Button b = new Button(user.getName(), () -> {
System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
});
diff --git a/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java b/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java
new file mode 100644
index 0000000..2014f43
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java
@@ -0,0 +1,44 @@
+package nl.andrewl.concord_client.model;
+
+import lombok.Getter;
+import nl.andrewl.concord_client.event.ChatHistoryListener;
+import nl.andrewl.concord_core.msg.types.Chat;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Stores information about a snippet of chat history that the client is
+ * currently viewing. This might be some older section of chats, or it could be
+ * the currently-in-use channel chats.
+ */
+public class ChatHistory {
+ @Getter
+ private List chats;
+
+ private final List chatHistoryListeners;
+
+ public ChatHistory() {
+ this.chats = new CopyOnWriteArrayList<>();
+ this.chatHistoryListeners = new CopyOnWriteArrayList<>();
+ }
+
+ public void setChats(List chats) {
+ this.chats.clear();
+ this.chats.addAll(chats);
+ this.chatHistoryListeners.forEach(listener -> listener.chatUpdated(this));
+ }
+
+ public void addChat(Chat chat) {
+ this.chats.add(chat);
+ this.chatHistoryListeners.forEach(listener -> listener.chatAdded(chat));
+ }
+
+ public void addListener(ChatHistoryListener listener) {
+ this.chatHistoryListeners.add(listener);
+ }
+
+ public void removeListener(ChatHistoryListener listener) {
+ this.chatHistoryListeners.remove(listener);
+ }
+}
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
new file mode 100644
index 0000000..3a44273
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java
@@ -0,0 +1,53 @@
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Getter
+public class ClientModel {
+ private UUID id;
+ private String nickname;
+ private ServerMetaData serverMetaData;
+
+ private UUID currentChannelId;
+ private List knownUsers;
+ private final ChatHistory chatHistory;
+
+ private final List modelListeners;
+
+ public ClientModel(UUID id, String nickname, UUID currentChannelId, ServerMetaData serverMetaData) {
+ this.modelListeners = new CopyOnWriteArrayList<>();
+ this.id = id;
+ this.nickname = nickname;
+ this.currentChannelId = currentChannelId;
+ this.serverMetaData = serverMetaData;
+ this.knownUsers = new ArrayList<>();
+ this.chatHistory = new ChatHistory();
+ }
+
+ public void setCurrentChannelId(UUID newChannelId) {
+ UUID oldId = this.currentChannelId;
+ this.currentChannelId = newChannelId;
+ this.modelListeners.forEach(listener -> listener.channelMoved(oldId, newChannelId));
+ }
+
+ public void setKnownUsers(List users) {
+ this.knownUsers = users;
+ this.modelListeners.forEach(listener -> listener.usersUpdated(this.knownUsers));
+ }
+
+ public void addListener(ClientModelListener listener) {
+ this.modelListeners.add(listener);
+ }
+
+ public void removeListener(ClientModelListener listener) {
+ this.modelListeners.remove(listener);
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java
index ec536e5..16e8253 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java
@@ -58,4 +58,16 @@ public class Chat implements Message {
public String toString() {
return String.format("%s: %s", this.senderNickname, this.message);
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (o.getClass().equals(this.getClass())) {
+ Chat other = (Chat) o;
+ return this.getSenderId().equals(other.getSenderId()) &&
+ this.getTimestamp() == other.getTimestamp() &&
+ this.getSenderNickname().equals(other.getSenderNickname()) &&
+ this.message.length() == other.message.length();
+ }
+ return false;
+ }
}
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 07a8894..a1cc69e 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
@@ -8,7 +8,10 @@ import nl.andrewl.concord_core.msg.Message;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
+import java.util.stream.Collectors;
import static nl.andrewl.concord_core.msg.MessageUtils.*;
@@ -49,12 +52,36 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
@NoArgsConstructor
@AllArgsConstructor
public class ChatHistoryRequest implements Message {
- public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}
-
- private UUID sourceId;
- private Source sourceType;
+ private UUID channelId;
private String query;
+ public ChatHistoryRequest(UUID channelId, Map params) {
+ this.channelId = channelId;
+ this.query = params.entrySet().stream()
+ .map(entry -> {
+ if (entry.getKey().contains(";") || entry.getKey().contains("=")) {
+ throw new IllegalArgumentException("Parameter key \"" + entry.getKey() + "\" contains invalid characters.");
+ }
+ if (entry.getValue().contains(";") || entry.getValue().contains("=")) {
+ throw new IllegalArgumentException("Parameter value \"" + entry.getValue() + "\" contains invalid characters.");
+ }
+ return entry.getKey() + "=" + entry.getValue();
+ })
+ .collect(Collectors.joining(";"));
+ }
+
+ public Map getQueryAsMap() {
+ String[] pairs = this.query.split(";");
+ if (pairs.length == 0) return Map.of();
+ Map params = new HashMap<>(pairs.length);
+ for (var pair : pairs) {
+ String[] keyAndValue = pair.split("=");
+ if (keyAndValue.length != 2) continue;
+ params.put(keyAndValue[0], keyAndValue[1]);
+ }
+ return params;
+ }
+
@Override
public int getByteCount() {
return UUID_BYTES + Integer.BYTES + getByteSize(this.query);
@@ -62,15 +89,13 @@ public class ChatHistoryRequest implements Message {
@Override
public void write(DataOutputStream o) throws IOException {
- writeUUID(this.sourceId, o);
- writeEnum(this.sourceType, o);
+ writeUUID(this.channelId, o);
writeString(this.query, o);
}
@Override
public void read(DataInputStream i) throws IOException {
- this.sourceId = readUUID(i);
- this.sourceType = readEnum(Source.class, i);
+ this.channelId = readUUID(i);
this.query = readString(i);
}
}
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 7f74e12..bc2ebec 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
@@ -20,13 +20,12 @@ import java.util.UUID;
@NoArgsConstructor
@AllArgsConstructor
public class ChatHistoryResponse implements Message {
- private UUID sourceId;
- private ChatHistoryRequest.Source sourceType;
+ private UUID channelId;
List messages;
@Override
public int getByteCount() {
- int count = Long.BYTES + Integer.BYTES + Integer.BYTES;
+ int count = Long.BYTES + Integer.BYTES;
for (var message : this.messages) {
count += message.getByteCount();
}
@@ -35,8 +34,7 @@ public class ChatHistoryResponse implements Message {
@Override
public void write(DataOutputStream o) throws IOException {
- MessageUtils.writeUUID(this.sourceId, o);
- MessageUtils.writeEnum(this.sourceType, o);
+ MessageUtils.writeUUID(this.channelId, o);
o.writeInt(messages.size());
for (var message : this.messages) {
message.write(o);
@@ -45,8 +43,7 @@ public class ChatHistoryResponse implements Message {
@Override
public void read(DataInputStream i) throws IOException {
- this.sourceId = MessageUtils.readUUID(i);
- this.sourceType = MessageUtils.readEnum(ChatHistoryRequest.Source.class, i);
+ this.channelId = MessageUtils.readUUID(i);
int messageCount = i.readInt();
Chat[] messages = new Chat[messageCount];
for (int k = 0; k < messageCount; k++) {
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 642c5bd..96eecfc 100644
--- a/server/src/main/java/nl/andrewl/concord_server/Channel.java
+++ b/server/src/main/java/nl/andrewl/concord_server/Channel.java
@@ -89,6 +89,6 @@ public class Channel {
@Override
public String toString() {
- return this.name;
+ return this.name + " (" + this.id + ")";
}
}
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 c38d20e..f07e714 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java
@@ -49,6 +49,6 @@ public class ChannelManager {
channel.addClient(client);
client.setCurrentChannel(channel);
client.sendToClient(new MoveToChannel(channel.getId()));
- System.out.println("Moved client " + client.getClientNickname() + " to channel " + channel.getName());
+ System.out.println("Moved client " + client + " to channel " + 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 dea8a95..19f7595 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java
@@ -2,7 +2,6 @@ package nl.andrewl.concord_server;
import lombok.Getter;
import lombok.Setter;
-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;
@@ -17,7 +16,6 @@ import java.util.UUID;
* This thread is responsible for handling the connection to a single client of
* a server. The client thread acts as the server's representation of a client.
*/
-@Log
public class ClientThread extends Thread {
private final Socket socket;
private final DataInputStream in;
@@ -75,7 +73,7 @@ public class ClientThread extends Thread {
public void run() {
this.running = true;
if (!identifyClient()) {
- log.warning("Could not identify the client; aborting connection.");
+ System.err.println("Could not identify the client; aborting connection.");
this.running = false;
}
@@ -84,7 +82,6 @@ public class ClientThread extends Thread {
var msg = Serializer.readMessage(this.in);
this.server.getEventManager().handle(msg, this);
} catch (IOException e) {
- log.info("Client disconnected: " + e.getMessage());
this.running = false;
}
}
@@ -124,4 +121,9 @@ public class ClientThread extends Thread {
}
return false;
}
+
+ @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 5e531b3..05bef83 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
@@ -22,16 +22,19 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
-@Log
+/**
+ * The main server implementation, which handles accepting new clients.
+ */
public class ConcordServer implements Runnable {
private final Map clients;
- private final int port;
- private final String name;
+ private volatile boolean running;
+
+ @Getter
+ private final ServerConfig config;
@Getter
private final IdProvider idProvider;
@Getter
private final Nitrite db;
- private volatile boolean running;
@Getter
private final ExecutorService executorService;
@Getter
@@ -41,9 +44,7 @@ public class ConcordServer implements Runnable {
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.config = ServerConfig.loadOrCreate(Path.of("server-config.json"), idProvider);
this.db = Nitrite.builder()
.filePath("concord-server.db")
.openOrCreate();
@@ -67,15 +68,15 @@ public class ConcordServer implements Runnable {
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());
+ System.out.println("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());
+ System.out.println("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());
+ System.out.println("Adding message index to collection for channel " + channel.getName());
col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext));
}
}
@@ -92,13 +93,13 @@ public class ConcordServer implements Runnable {
*/
public void registerClient(Identification identification, ClientThread clientThread) {
var id = this.idProvider.newId();
- log.info("Registering new client " + identification.getNickname() + " with id " + id);
+ System.out.printf("Client \"%s\" joined with id %s.\n", identification.getNickname(), 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(
- this.name,
+ this.config.name(),
this.channelManager.getChannels().stream()
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
.sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
@@ -110,6 +111,7 @@ public class ConcordServer implements Runnable {
// 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);
+ System.out.println("Moved client " + clientThread + " to " + defaultChannel);
}
/**
@@ -122,6 +124,7 @@ public class ConcordServer implements Runnable {
if (client != null) {
client.getCurrentChannel().removeClient(client);
client.shutdown();
+ System.out.println("Client " + client + " has disconnected.");
}
}
@@ -130,11 +133,15 @@ public class ConcordServer implements Runnable {
this.running = true;
ServerSocket serverSocket;
try {
- serverSocket = new ServerSocket(this.port);
- log.info("Opened server on port " + this.port);
+ serverSocket = new ServerSocket(this.config.port());
+ StringBuilder startupMessage = new StringBuilder();
+ startupMessage.append("Opened server on port ").append(config.port()).append("\n");
+ for (var channel : this.channelManager.getChannels()) {
+ startupMessage.append("\tChannel \"").append(channel).append('\n');
+ }
+ System.out.println(startupMessage);
while (this.running) {
Socket socket = serverSocket.accept();
- log.info("Accepted new socket connection from " + socket.getInetAddress().getHostAddress());
ClientThread clientThread = new ClientThread(socket, this);
clientThread.start();
}
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 b98fc3a..dd2680a 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
@@ -9,10 +9,15 @@ import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
-@Log
public record ServerConfig(
String name,
int port,
+
+ // Global Channel configuration
+ int chatHistoryMaxCount,
+ int chatHistoryDefaultCount,
+ int maxMessageLength,
+
ChannelConfig[] channels
) {
@@ -29,6 +34,9 @@ public record ServerConfig(
config = new ServerConfig(
"My Concord Server",
8123,
+ 100,
+ 50,
+ 8192,
new ServerConfig.ChannelConfig[]{
new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")
}
@@ -38,14 +46,14 @@ public record ServerConfig(
} catch (IOException e) {
throw new UncheckedIOException(e);
}
- log.info(filePath + " does not exist. Creating it with initial values. Edit and restart to apply changes.");
+ System.err.println(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);
+ System.out.println("Loaded configuration from " + filePath);
}
return config;
}
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 70dc421..e553ee3 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
@@ -3,41 +3,72 @@ 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.Channel;
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 org.dizitart.no2.*;
+import org.dizitart.no2.filters.Filters;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
+/**
+ * Handles client requests for sections of chat history for a particular channel.
+ */
public class ChatHistoryRequestHandler implements MessageHandler {
@Override
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
- var optionalChannel = server.getChannelManager().getChannelById(msg.getSourceId());
+ var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
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.Descending)
- .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)
- ));
+ var params = msg.getQueryAsMap();
+ Long count = this.getOrDefault(params, "count", (long) server.getConfig().chatHistoryDefaultCount());
+ if (count > server.getConfig().chatHistoryMaxCount()) {
+ return;
}
- col.close();
- chats.sort(Comparator.comparingLong(Chat::getTimestamp));
- client.sendToClient(new ChatHistoryResponse(msg.getSourceId(), msg.getSourceType(), chats));
+ Long from = this.getOrDefault(params, "from", null);
+ Long to = this.getOrDefault(params, "to", null);
+ client.sendToClient(this.getResponse(channel, count, from, to));
}
}
+
+ private Long getOrDefault(Map params, String key, Long defaultValue) {
+ String value = params.get(key);
+ if (value == null) return defaultValue;
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ private ChatHistoryResponse getResponse(Channel channel, long count, Long from, Long to) {
+ var col = channel.getServer().getDb().getCollection("channel-" + channel.getId());
+ Cursor cursor;
+ FindOptions options = FindOptions.sort("timestamp", SortOrder.Descending).thenLimit(0, (int) count);
+ List filters = new ArrayList<>(2);
+ if (from != null) {
+ filters.add(Filters.gt("timestamp", from));
+ }
+ if (to != null) {
+ filters.add(Filters.lt("timestamp", to));
+ }
+ if (filters.isEmpty()) {
+ cursor = col.find(options);
+ } else {
+ cursor = col.find(Filters.and(filters.toArray(new Filter[0])), options);
+ }
+
+ List chats = new ArrayList<>((int) count);
+ 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();
+ chats.sort(Comparator.comparingLong(Chat::getTimestamp));
+ return new ChatHistoryResponse(channel.getId(), chats);
+ }
}
diff --git a/server/src/main/resources/nl/andrewl/concord_server/logback.xml b/server/src/main/resources/nl/andrewl/concord_server/logback.xml
deleted file mode 100644
index 57779e4..0000000
--- a/server/src/main/resources/nl/andrewl/concord_server/logback.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- %d{dd.MM.yyyy HH:mm:ss} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) %highlight(%-6level) %msg%n
-
-
-
-
-
-
-