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>
|
<dependency>
|
||||||
<groupId>com.googlecode.lanterna</groupId>
|
<groupId>com.googlecode.lanterna</groupId>
|
||||||
<artifactId>lanterna</artifactId>
|
<artifactId>lanterna</artifactId>
|
||||||
<version>3.1.1</version>
|
<version>3.2.0-alpha1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</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.DefaultTerminalFactory;
|
||||||
import com.googlecode.lanterna.terminal.Terminal;
|
import com.googlecode.lanterna.terminal.Terminal;
|
||||||
import lombok.Getter;
|
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.gui.MainWindow;
|
||||||
|
import nl.andrewl.concord_client.model.ClientModel;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.Serializer;
|
import nl.andrewl.concord_core.msg.Serializer;
|
||||||
import nl.andrewl.concord_core.msg.types.*;
|
import nl.andrewl.concord_core.msg.types.*;
|
||||||
|
@ -18,52 +22,41 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class ConcordClient implements Runnable {
|
public class ConcordClient implements Runnable {
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
private final DataInputStream in;
|
private final DataInputStream in;
|
||||||
private final DataOutputStream out;
|
private final DataOutputStream out;
|
||||||
private final UUID id;
|
|
||||||
private final String nickname;
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
private final ClientModel model;
|
||||||
private UUID currentChannelId;
|
|
||||||
@Getter
|
private final EventManager eventManager;
|
||||||
private ServerMetaData serverMetaData;
|
|
||||||
private final Set<ClientMessageListener> messageListeners;
|
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
|
|
||||||
public ConcordClient(String host, int port, String nickname) throws IOException {
|
public ConcordClient(String host, int port, String nickname) throws IOException {
|
||||||
|
this.eventManager = new EventManager(this);
|
||||||
this.socket = new Socket(host, port);
|
this.socket = new Socket(host, port);
|
||||||
this.in = new DataInputStream(this.socket.getInputStream());
|
this.in = new DataInputStream(this.socket.getInputStream());
|
||||||
this.out = new DataOutputStream(this.socket.getOutputStream());
|
this.out = new DataOutputStream(this.socket.getOutputStream());
|
||||||
this.nickname = nickname;
|
|
||||||
Serializer.writeMessage(new Identification(nickname), this.out);
|
Serializer.writeMessage(new Identification(nickname), this.out);
|
||||||
Message reply = Serializer.readMessage(this.in);
|
Message reply = Serializer.readMessage(this.in);
|
||||||
if (reply instanceof ServerWelcome welcome) {
|
if (reply instanceof ServerWelcome welcome) {
|
||||||
this.id = welcome.getClientId();
|
this.model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getMetaData());
|
||||||
this.currentChannelId = welcome.getCurrentChannelId();
|
|
||||||
this.serverMetaData = welcome.getMetaData();
|
|
||||||
|
|
||||||
// Start fetching initial data for the channel we were initially put into.
|
// Start fetching initial data for the channel we were initially put into.
|
||||||
this.sendMessage(new ChannelUsersRequest(this.currentChannelId));
|
this.sendMessage(new ChannelUsersRequest(this.model.getCurrentChannelId()));
|
||||||
this.sendMessage(new ChatHistoryRequest(this.currentChannelId, ChatHistoryRequest.Source.CHANNEL, ""));
|
this.sendMessage(new ChatHistoryRequest(this.model.getCurrentChannelId(), ""));
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Unexpected response from the server after sending identification message.");
|
throw new IOException("Unexpected response from the server after sending identification message.");
|
||||||
}
|
}
|
||||||
this.messageListeners = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addListener(ClientMessageListener listener) {
|
// Add event listeners.
|
||||||
this.messageListeners.add(listener);
|
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
|
||||||
}
|
this.eventManager.addHandler(ChannelUsersResponse.class, new ChannelUsersResponseHandler());
|
||||||
|
this.eventManager.addHandler(ChatHistoryResponse.class, new ChatHistoryResponseHandler());
|
||||||
public void removeListener(ClientMessageListener listener) {
|
this.eventManager.addHandler(Chat.class, (msg, client) -> client.getModel().getChatHistory().addChat(msg));
|
||||||
this.messageListeners.remove(listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(Message message) throws IOException {
|
public void sendMessage(Message message) throws IOException {
|
||||||
|
@ -71,7 +64,7 @@ public class ConcordClient implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendChat(String message) throws IOException {
|
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() {
|
public void shutdown() {
|
||||||
|
@ -91,9 +84,7 @@ public class ConcordClient implements Runnable {
|
||||||
while (this.running) {
|
while (this.running) {
|
||||||
try {
|
try {
|
||||||
Message msg = Serializer.readMessage(this.in);
|
Message msg = Serializer.readMessage(this.in);
|
||||||
for (var listener : this.messageListeners) {
|
this.eventManager.handle(msg);
|
||||||
listener.messageReceived(this, msg);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
this.running = false;
|
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());
|
super(new BorderLayout());
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.chatList = new ChatList();
|
this.chatList = new ChatList();
|
||||||
|
this.client.getModel().getChatHistory().addListener(this.chatList);
|
||||||
this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
|
this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
|
||||||
this.inputTextBox.setCaretWarp(true);
|
this.inputTextBox.setCaretWarp(true);
|
||||||
this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
|
this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
|
||||||
|
@ -37,7 +38,6 @@ public class ChannelChatBox extends Panel {
|
||||||
String text = inputTextBox.getText();
|
String text = inputTextBox.getText();
|
||||||
if (text != null && !text.isBlank()) {
|
if (text != null && !text.isBlank()) {
|
||||||
try {
|
try {
|
||||||
System.out.println("Sending: " + text.trim());
|
|
||||||
client.sendChat(text.trim());
|
client.sendChat(text.trim());
|
||||||
inputTextBox.setText("");
|
inputTextBox.setText("");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -53,8 +53,8 @@ public class ChannelChatBox extends Panel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshBorder() {
|
public void refreshBorder() {
|
||||||
String name = client.getServerMetaData().getChannels().stream()
|
String name = client.getModel().getServerMetaData().getChannels().stream()
|
||||||
.filter(channelData -> channelData.getId().equals(client.getCurrentChannelId()))
|
.filter(channelData -> channelData.getId().equals(client.getModel().getCurrentChannelId()))
|
||||||
.findAny().orElseThrow().getName();
|
.findAny().orElseThrow().getName();
|
||||||
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
|
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
|
||||||
this.chatBorder = Borders.doubleLine("#" + name);
|
this.chatBorder = Borders.doubleLine("#" + name);
|
||||||
|
|
|
@ -6,6 +6,10 @@ import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
|
||||||
import java.io.IOException;
|
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 {
|
public class ChannelList extends Panel {
|
||||||
private final ConcordClient client;
|
private final ConcordClient client;
|
||||||
public ChannelList(ConcordClient client) {
|
public ChannelList(ConcordClient client) {
|
||||||
|
@ -15,13 +19,13 @@ public class ChannelList extends Panel {
|
||||||
|
|
||||||
public void setChannels() {
|
public void setChannels() {
|
||||||
this.removeAllComponents();
|
this.removeAllComponents();
|
||||||
for (var channel : this.client.getServerMetaData().getChannels()) {
|
for (var channel : this.client.getModel().getServerMetaData().getChannels()) {
|
||||||
String name = channel.getName();
|
String name = channel.getName();
|
||||||
if (client.getCurrentChannelId().equals(channel.getId())) {
|
if (client.getModel().getCurrentChannelId().equals(channel.getId())) {
|
||||||
name = "*" + name;
|
name = "*" + name;
|
||||||
}
|
}
|
||||||
Button b = new Button(name, () -> {
|
Button b = new Button(name, () -> {
|
||||||
if (!client.getCurrentChannelId().equals(channel.getId())) {
|
if (!client.getModel().getCurrentChannelId().equals(channel.getId())) {
|
||||||
try {
|
try {
|
||||||
client.sendMessage(new MoveToChannel(channel.getId()));
|
client.sendMessage(new MoveToChannel(channel.getId()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
package nl.andrewl.concord_client.gui;
|
package nl.andrewl.concord_client.gui;
|
||||||
|
|
||||||
import com.googlecode.lanterna.gui2.AbstractListBox;
|
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;
|
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.
|
* 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() {
|
protected ListItemRenderer<Chat, ChatList> createDefaultListItemRenderer() {
|
||||||
return new ChatRenderer();
|
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 {
|
try {
|
||||||
var client = new ConcordClient(host, port, nickname);
|
var client = new ConcordClient(host, port, nickname);
|
||||||
var chatPanel = new ServerPanel(client, this);
|
var chatPanel = new ServerPanel(client, this);
|
||||||
client.addListener(chatPanel);
|
client.getModel().addListener(chatPanel);
|
||||||
new Thread(client).start();
|
new Thread(client).start();
|
||||||
this.setComponent(chatPanel);
|
this.setComponent(chatPanel);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
@ -2,12 +2,12 @@ package nl.andrewl.concord_client.gui;
|
||||||
|
|
||||||
import com.googlecode.lanterna.gui2.*;
|
import com.googlecode.lanterna.gui2.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewl.concord_client.ClientMessageListener;
|
|
||||||
import nl.andrewl.concord_client.ConcordClient;
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_client.event.ClientModelListener;
|
||||||
import nl.andrewl.concord_core.msg.types.*;
|
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
|
* 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
|
* meta information in the sidebars which provides the user with a list of all
|
||||||
* threads and users in the server.
|
* threads and users in the server.
|
||||||
*/
|
*/
|
||||||
public class ServerPanel extends Panel implements ClientMessageListener {
|
public class ServerPanel extends Panel implements ClientModelListener {
|
||||||
@Getter
|
@Getter
|
||||||
private final ChannelChatBox channelChatBox;
|
private final ChannelChatBox channelChatBox;
|
||||||
private final ChannelList channelList;
|
private final ChannelList channelList;
|
||||||
private final UserList userList;
|
private final UserList userList;
|
||||||
|
|
||||||
private final ConcordClient client;
|
|
||||||
private final TextGUIThread guiThread;
|
private final TextGUIThread guiThread;
|
||||||
|
|
||||||
public ServerPanel(ConcordClient client, Window window) {
|
public ServerPanel(ConcordClient client, Window window) {
|
||||||
super(new BorderLayout());
|
super(new BorderLayout());
|
||||||
this.guiThread = window.getTextGUI().getGUIThread();
|
this.guiThread = window.getTextGUI().getGUIThread();
|
||||||
this.client = client;
|
|
||||||
this.channelChatBox = new ChannelChatBox(client, window);
|
this.channelChatBox = new ChannelChatBox(client, window);
|
||||||
this.channelList = new ChannelList(client);
|
this.channelList = new ChannelList(client);
|
||||||
this.channelList.setChannels();
|
this.channelList.setChannels();
|
||||||
|
@ -47,36 +45,19 @@ public class ServerPanel extends Panel implements ClientMessageListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void messageReceived(ConcordClient client, Message message) {
|
public void channelMoved(UUID oldChannelId, UUID newChannelId) {
|
||||||
if (message instanceof Chat chat) {
|
this.getTextGUI().getGUIThread().invokeLater(() -> {
|
||||||
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.channelList.setChannels();
|
||||||
this.channelChatBox.getChatList().clearItems();
|
this.channelChatBox.getChatList().clearItems();
|
||||||
this.channelChatBox.refreshBorder();
|
this.channelChatBox.refreshBorder();
|
||||||
this.channelChatBox.getInputTextBox().takeFocus();
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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_client.ConcordClient;
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class UserList extends Panel {
|
public class UserList extends Panel {
|
||||||
private final ConcordClient client;
|
private final ConcordClient client;
|
||||||
|
|
||||||
|
@ -15,9 +17,9 @@ public class UserList extends Panel {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUsers(ChannelUsersResponse usersResponse) {
|
public void updateUsers(List<ChannelUsersResponse.UserData> usersResponse) {
|
||||||
this.removeAllComponents();
|
this.removeAllComponents();
|
||||||
for (var user : usersResponse.getUsers()) {
|
for (var user : usersResponse) {
|
||||||
Button b = new Button(user.getName(), () -> {
|
Button b = new Button(user.getName(), () -> {
|
||||||
System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
|
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() {
|
public String toString() {
|
||||||
return String.format("%s: %s", this.senderNickname, this.message);
|
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.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
|
@ -49,12 +52,36 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ChatHistoryRequest implements Message {
|
public class ChatHistoryRequest implements Message {
|
||||||
public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}
|
private UUID channelId;
|
||||||
|
|
||||||
private UUID sourceId;
|
|
||||||
private Source sourceType;
|
|
||||||
private String query;
|
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
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return UUID_BYTES + Integer.BYTES + getByteSize(this.query);
|
return UUID_BYTES + Integer.BYTES + getByteSize(this.query);
|
||||||
|
@ -62,15 +89,13 @@ public class ChatHistoryRequest implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
writeUUID(this.sourceId, o);
|
writeUUID(this.channelId, o);
|
||||||
writeEnum(this.sourceType, o);
|
|
||||||
writeString(this.query, o);
|
writeString(this.query, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.sourceId = readUUID(i);
|
this.channelId = readUUID(i);
|
||||||
this.sourceType = readEnum(Source.class, i);
|
|
||||||
this.query = readString(i);
|
this.query = readString(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,12 @@ import java.util.UUID;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ChatHistoryResponse implements Message {
|
public class ChatHistoryResponse implements Message {
|
||||||
private UUID sourceId;
|
private UUID channelId;
|
||||||
private ChatHistoryRequest.Source sourceType;
|
|
||||||
List<Chat> messages;
|
List<Chat> messages;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
int count = Long.BYTES + Integer.BYTES + Integer.BYTES;
|
int count = Long.BYTES + Integer.BYTES;
|
||||||
for (var message : this.messages) {
|
for (var message : this.messages) {
|
||||||
count += message.getByteCount();
|
count += message.getByteCount();
|
||||||
}
|
}
|
||||||
|
@ -35,8 +34,7 @@ public class ChatHistoryResponse implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
MessageUtils.writeUUID(this.sourceId, o);
|
MessageUtils.writeUUID(this.channelId, o);
|
||||||
MessageUtils.writeEnum(this.sourceType, o);
|
|
||||||
o.writeInt(messages.size());
|
o.writeInt(messages.size());
|
||||||
for (var message : this.messages) {
|
for (var message : this.messages) {
|
||||||
message.write(o);
|
message.write(o);
|
||||||
|
@ -45,8 +43,7 @@ public class ChatHistoryResponse implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.sourceId = MessageUtils.readUUID(i);
|
this.channelId = MessageUtils.readUUID(i);
|
||||||
this.sourceType = MessageUtils.readEnum(ChatHistoryRequest.Source.class, i);
|
|
||||||
int messageCount = i.readInt();
|
int messageCount = i.readInt();
|
||||||
Chat[] messages = new Chat[messageCount];
|
Chat[] messages = new Chat[messageCount];
|
||||||
for (int k = 0; k < messageCount; k++) {
|
for (int k = 0; k < messageCount; k++) {
|
||||||
|
|
|
@ -89,6 +89,6 @@ public class Channel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.name;
|
return this.name + " (" + this.id + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,6 @@ public class ChannelManager {
|
||||||
channel.addClient(client);
|
channel.addClient(client);
|
||||||
client.setCurrentChannel(channel);
|
client.setCurrentChannel(channel);
|
||||||
client.sendToClient(new MoveToChannel(channel.getId()));
|
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.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.java.Log;
|
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.Serializer;
|
import nl.andrewl.concord_core.msg.Serializer;
|
||||||
import nl.andrewl.concord_core.msg.types.Identification;
|
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
|
* 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.
|
* a server. The client thread acts as the server's representation of a client.
|
||||||
*/
|
*/
|
||||||
@Log
|
|
||||||
public class ClientThread extends Thread {
|
public class ClientThread extends Thread {
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
private final DataInputStream in;
|
private final DataInputStream in;
|
||||||
|
@ -75,7 +73,7 @@ public class ClientThread extends Thread {
|
||||||
public void run() {
|
public void run() {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
if (!identifyClient()) {
|
if (!identifyClient()) {
|
||||||
log.warning("Could not identify the client; aborting connection.");
|
System.err.println("Could not identify the client; aborting connection.");
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +82,6 @@ public class ClientThread extends Thread {
|
||||||
var msg = Serializer.readMessage(this.in);
|
var msg = Serializer.readMessage(this.in);
|
||||||
this.server.getEventManager().handle(msg, this);
|
this.server.getEventManager().handle(msg, this);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.info("Client disconnected: " + e.getMessage());
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,4 +121,9 @@ public class ClientThread extends Thread {
|
||||||
}
|
}
|
||||||
return false;
|
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.concurrent.Executors;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Log
|
/**
|
||||||
|
* The main server implementation, which handles accepting new clients.
|
||||||
|
*/
|
||||||
public class ConcordServer implements Runnable {
|
public class ConcordServer implements Runnable {
|
||||||
private final Map<UUID, ClientThread> clients;
|
private final Map<UUID, ClientThread> clients;
|
||||||
private final int port;
|
private volatile boolean running;
|
||||||
private final String name;
|
|
||||||
|
@Getter
|
||||||
|
private final ServerConfig config;
|
||||||
@Getter
|
@Getter
|
||||||
private final IdProvider idProvider;
|
private final IdProvider idProvider;
|
||||||
@Getter
|
@Getter
|
||||||
private final Nitrite db;
|
private final Nitrite db;
|
||||||
private volatile boolean running;
|
|
||||||
@Getter
|
@Getter
|
||||||
private final ExecutorService executorService;
|
private final ExecutorService executorService;
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -41,9 +44,7 @@ public class ConcordServer implements Runnable {
|
||||||
|
|
||||||
public ConcordServer() {
|
public ConcordServer() {
|
||||||
this.idProvider = new UUIDProvider();
|
this.idProvider = new UUIDProvider();
|
||||||
ServerConfig config = ServerConfig.loadOrCreate(Path.of("server-config.json"), idProvider);
|
this.config = ServerConfig.loadOrCreate(Path.of("server-config.json"), idProvider);
|
||||||
this.port = config.port();
|
|
||||||
this.name = config.name();
|
|
||||||
this.db = Nitrite.builder()
|
this.db = Nitrite.builder()
|
||||||
.filePath("concord-server.db")
|
.filePath("concord-server.db")
|
||||||
.openOrCreate();
|
.openOrCreate();
|
||||||
|
@ -67,15 +68,15 @@ public class ConcordServer implements Runnable {
|
||||||
for (var channel : this.channelManager.getChannels()) {
|
for (var channel : this.channelManager.getChannels()) {
|
||||||
var col = channel.getMessageCollection();
|
var col = channel.getMessageCollection();
|
||||||
if (!col.hasIndex("timestamp")) {
|
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));
|
col.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique));
|
||||||
}
|
}
|
||||||
if (!col.hasIndex("senderNickname")) {
|
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));
|
col.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext));
|
||||||
}
|
}
|
||||||
if (!col.hasIndex("message")) {
|
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));
|
col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,13 +93,13 @@ public class ConcordServer implements Runnable {
|
||||||
*/
|
*/
|
||||||
public void registerClient(Identification identification, ClientThread clientThread) {
|
public void registerClient(Identification identification, ClientThread clientThread) {
|
||||||
var id = this.idProvider.newId();
|
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);
|
this.clients.put(id, clientThread);
|
||||||
clientThread.setClientId(id);
|
clientThread.setClientId(id);
|
||||||
clientThread.setClientNickname(identification.getNickname());
|
clientThread.setClientNickname(identification.getNickname());
|
||||||
// Send a welcome reply containing all the initial server info the client needs.
|
// Send a welcome reply containing all the initial server info the client needs.
|
||||||
ServerMetaData metaData = new ServerMetaData(
|
ServerMetaData metaData = new ServerMetaData(
|
||||||
this.name,
|
this.config.name(),
|
||||||
this.channelManager.getChannels().stream()
|
this.channelManager.getChannels().stream()
|
||||||
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
|
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
|
||||||
.sorted(Comparator.comparing(ServerMetaData.ChannelData::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.
|
// 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);
|
defaultChannel.addClient(clientThread);
|
||||||
clientThread.setCurrentChannel(defaultChannel);
|
clientThread.setCurrentChannel(defaultChannel);
|
||||||
|
System.out.println("Moved client " + clientThread + " to " + defaultChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,6 +124,7 @@ public class ConcordServer implements Runnable {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.getCurrentChannel().removeClient(client);
|
client.getCurrentChannel().removeClient(client);
|
||||||
client.shutdown();
|
client.shutdown();
|
||||||
|
System.out.println("Client " + client + " has disconnected.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,11 +133,15 @@ public class ConcordServer implements Runnable {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
ServerSocket serverSocket;
|
ServerSocket serverSocket;
|
||||||
try {
|
try {
|
||||||
serverSocket = new ServerSocket(this.port);
|
serverSocket = new ServerSocket(this.config.port());
|
||||||
log.info("Opened server on port " + this.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) {
|
while (this.running) {
|
||||||
Socket socket = serverSocket.accept();
|
Socket socket = serverSocket.accept();
|
||||||
log.info("Accepted new socket connection from " + socket.getInetAddress().getHostAddress());
|
|
||||||
ClientThread clientThread = new ClientThread(socket, this);
|
ClientThread clientThread = new ClientThread(socket, this);
|
||||||
clientThread.start();
|
clientThread.start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,15 @@ import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@Log
|
|
||||||
public record ServerConfig(
|
public record ServerConfig(
|
||||||
String name,
|
String name,
|
||||||
int port,
|
int port,
|
||||||
|
|
||||||
|
// Global Channel configuration
|
||||||
|
int chatHistoryMaxCount,
|
||||||
|
int chatHistoryDefaultCount,
|
||||||
|
int maxMessageLength,
|
||||||
|
|
||||||
ChannelConfig[] channels
|
ChannelConfig[] channels
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -29,6 +34,9 @@ public record ServerConfig(
|
||||||
config = new ServerConfig(
|
config = new ServerConfig(
|
||||||
"My Concord Server",
|
"My Concord Server",
|
||||||
8123,
|
8123,
|
||||||
|
100,
|
||||||
|
50,
|
||||||
|
8192,
|
||||||
new ServerConfig.ChannelConfig[]{
|
new ServerConfig.ChannelConfig[]{
|
||||||
new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")
|
new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")
|
||||||
}
|
}
|
||||||
|
@ -38,14 +46,14 @@ public record ServerConfig(
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class);
|
config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
log.info("Loaded configuration from " + filePath);
|
System.out.println("Loaded configuration from " + filePath);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,62 @@ package nl.andrewl.concord_server.event;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
|
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.ClientThread;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
import org.dizitart.no2.Document;
|
import org.dizitart.no2.*;
|
||||||
import org.dizitart.no2.FindOptions;
|
import org.dizitart.no2.filters.Filters;
|
||||||
import org.dizitart.no2.SortOrder;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles client requests for sections of chat history for a particular channel.
|
||||||
|
*/
|
||||||
public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequest> {
|
public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequest> {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
|
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()) {
|
if (optionalChannel.isPresent()) {
|
||||||
var channel = optionalChannel.get();
|
var channel = optionalChannel.get();
|
||||||
System.out.println("Looking for chats in channel-" + channel.getId());
|
var params = msg.getQueryAsMap();
|
||||||
var col = server.getDb().getCollection("channel-" + channel.getId());
|
Long count = this.getOrDefault(params, "count", (long) server.getConfig().chatHistoryDefaultCount());
|
||||||
var cursor = col.find(
|
if (count > server.getConfig().chatHistoryMaxCount()) {
|
||||||
FindOptions.sort("timestamp", SortOrder.Descending)
|
return;
|
||||||
.thenLimit(0, 10)
|
}
|
||||||
);
|
Long from = this.getOrDefault(params, "from", null);
|
||||||
List<Chat> chats = new ArrayList<>(10);
|
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) {
|
for (Document doc : cursor) {
|
||||||
chats.add(new Chat(
|
chats.add(new Chat(
|
||||||
doc.get("senderId", UUID.class),
|
doc.get("senderId", UUID.class),
|
||||||
|
@ -37,7 +69,6 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
||||||
}
|
}
|
||||||
col.close();
|
col.close();
|
||||||
chats.sort(Comparator.comparingLong(Chat::getTimestamp));
|
chats.sort(Comparator.comparingLong(Chat::getTimestamp));
|
||||||
client.sendToClient(new ChatHistoryResponse(msg.getSourceId(), msg.getSourceType(), chats));
|
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