Improved client event handling, and server consistency and chat history requests.
This commit is contained in:
parent
11e49696a4
commit
960f66cf13
|
@ -21,7 +21,7 @@
|
|||
<dependency>
|
||||
<groupId>com.googlecode.lanterna</groupId>
|
||||
<artifactId>lanterna</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<version>3.2.0-alpha1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -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;
|
||||
}
|
|
@ -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<ClientMessageListener> 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;
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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<ChannelUsersResponse.UserData> users) {}
|
||||
}
|
|
@ -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<Class<? extends Message>, List<MessageHandler<?>>> messageHandlers;
|
||||
private final ConcordClient client;
|
||||
|
||||
public EventManager(ConcordClient client) {
|
||||
this.client = client;
|
||||
this.messageHandlers = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public <T extends Message> void addHandler(Class<T> messageClass, MessageHandler<T> handler) {
|
||||
var handlers = this.messageHandlers.computeIfAbsent(messageClass, k -> new CopyOnWriteArrayList<>());
|
||||
handlers.add(handler);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Message> void handle(T message) {
|
||||
var handlers = this.messageHandlers.get(message.getClass());
|
||||
if (handlers != null) {
|
||||
for (var handler : handlers) {
|
||||
try {
|
||||
((MessageHandler<T>) handler).handle(message, this.client);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<T extends Message> {
|
||||
void handle(T msg, ConcordClient client) throws Exception;
|
||||
}
|
|
@ -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<MoveToChannel> {
|
||||
@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()));
|
||||
}
|
||||
}
|
|
@ -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<ChannelUsersResponse> {
|
||||
@Override
|
||||
public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception {
|
||||
client.getModel().setKnownUsers(msg.getUsers());
|
||||
}
|
||||
}
|
|
@ -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<ChatHistoryResponse> {
|
||||
@Override
|
||||
public void handle(ChatHistoryResponse msg, ConcordClient client) throws Exception {
|
||||
client.getModel().getChatHistory().setChats(msg.getMessages());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Chat, ChatList> {
|
||||
/**
|
||||
* 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<Chat, ChatList> implements ChatHistoryListener {
|
||||
/**
|
||||
* Adds one more item to the list box, at the end.
|
||||
*
|
||||
|
@ -28,4 +34,31 @@ public class ChatList extends AbstractListBox<Chat, ChatList> {
|
|||
protected ListItemRenderer<Chat, ChatList> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<ChannelUsersResponse.UserData> users) {
|
||||
this.guiThread.invokeLater(() -> {
|
||||
this.userList.updateUsers(users);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ChannelUsersResponse.UserData> 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());
|
||||
});
|
||||
|
|
|
@ -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<Chat> chats;
|
||||
|
||||
private final List<ChatHistoryListener> chatHistoryListeners;
|
||||
|
||||
public ChatHistory() {
|
||||
this.chats = new CopyOnWriteArrayList<>();
|
||||
this.chatHistoryListeners = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
public void setChats(List<Chat> 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);
|
||||
}
|
||||
}
|
|
@ -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<ChannelUsersResponse.UserData> knownUsers;
|
||||
private final ChatHistory chatHistory;
|
||||
|
||||
private final List<ClientModelListener> 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<ChannelUsersResponse.UserData> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String> 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<String, String> getQueryAsMap() {
|
||||
String[] pairs = this.query.split(";");
|
||||
if (pairs.length == 0) return Map.of();
|
||||
Map<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Chat> 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++) {
|
||||
|
|
|
@ -89,6 +89,6 @@ public class Channel {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
return this.name + " (" + this.id + ")";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UUID, ClientThread> 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<ChatHistoryRequest> {
|
||||
@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<Chat> 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<String, String> 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<Filter> 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<Chat> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{dd.MM.yyyy HH:mm:ss} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) %highlight(%-6level) %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
Loading…
Reference in New Issue