messages;
@@ -30,8 +33,8 @@ public class ChatHistoryResponse implements Message {
@Override
public void write(DataOutputStream o) throws IOException {
- o.writeLong(this.sourceId);
- writeEnum(this.sourceType, o);
+ MessageUtils.writeUUID(this.sourceId, o);
+ MessageUtils.writeEnum(this.sourceType, o);
o.writeInt(messages.size());
for (var message : this.messages) {
message.write(o);
@@ -40,8 +43,8 @@ public class ChatHistoryResponse implements Message {
@Override
public void read(DataInputStream i) throws IOException {
- this.sourceId = i.readInt();
- this.sourceType = readEnum(ChatHistoryRequest.Source.class, i);
+ this.sourceId = MessageUtils.readUUID(i);
+ this.sourceType = MessageUtils.readEnum(ChatHistoryRequest.Source.class, i);
int messageCount = i.readInt();
Chat[] messages = new Chat[messageCount];
for (int k = 0; k < messageCount; k++) {
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java
index 7f3e267..7a5d9a6 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java
@@ -3,6 +3,7 @@ package nl.andrewl.concord_core.msg.types;
import lombok.Data;
import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_core.msg.MessageUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -23,16 +24,16 @@ public class Identification implements Message {
@Override
public int getByteCount() {
- return getByteSize(this.nickname);
+ return MessageUtils.getByteSize(this.nickname);
}
@Override
public void write(DataOutputStream o) throws IOException {
- writeString(this.nickname, o);
+ MessageUtils.writeString(this.nickname, o);
}
@Override
public void read(DataInputStream i) throws IOException {
- this.nickname = readString(i);
+ this.nickname = MessageUtils.readString(i);
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java
new file mode 100644
index 0000000..d173913
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java
@@ -0,0 +1,46 @@
+package nl.andrewl.concord_core.msg.types;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nl.andrewl.concord_core.msg.Message;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.UUID;
+
+import static nl.andrewl.concord_core.msg.MessageUtils.*;
+
+/**
+ * A message that's sent to a client when they've been moved to another channel.
+ * This indicates to the client that they should perform the necessary requests
+ * to update their view to indicate that they're now in a different channel.
+ *
+ * Conversely, a client can send this request to the server to indicate that
+ * they would like to switch to the specified channel.
+ *
+ */
+@Data
+@NoArgsConstructor
+public class MoveToChannel implements Message {
+ private UUID channelId;
+
+ public MoveToChannel(UUID channelId) {
+ this.channelId = channelId;
+ }
+
+ @Override
+ public int getByteCount() {
+ return UUID_BYTES;
+ }
+
+ @Override
+ public void write(DataOutputStream o) throws IOException {
+ writeUUID(this.channelId, o);
+ }
+
+ @Override
+ public void read(DataInputStream i) throws IOException {
+ this.channelId = readUUID(i);
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java
new file mode 100644
index 0000000..ea2612e
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java
@@ -0,0 +1,68 @@
+package nl.andrewl.concord_core.msg.types;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nl.andrewl.concord_core.msg.Message;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import static nl.andrewl.concord_core.msg.MessageUtils.*;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ServerMetaData implements Message {
+ private String name;
+ private List channels;
+
+ @Override
+ public int getByteCount() {
+ return getByteSize(this.name) + getByteSize(this.channels);
+ }
+
+ @Override
+ public void write(DataOutputStream o) throws IOException {
+ writeString(this.name, o);
+ writeList(this.channels, o);
+ }
+
+ @Override
+ public void read(DataInputStream i) throws IOException {
+ this.name = readString(i);
+ try {
+ this.channels = readList(ChannelData.class, i);
+ } catch (ReflectiveOperationException e) {
+ throw new IOException("Reflection exception", e);
+ }
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ChannelData implements Message {
+ private UUID id;
+ private String name;
+
+ @Override
+ public int getByteCount() {
+ return UUID_BYTES + getByteSize(this.name);
+ }
+
+ @Override
+ public void write(DataOutputStream o) throws IOException {
+ writeUUID(this.id, o);
+ writeString(this.name, o);
+ }
+
+ @Override
+ public void read(DataInputStream i) throws IOException {
+ this.id = readUUID(i);
+ this.name = readString(i);
+ }
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java
index 8fd6a90..167de23 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java
@@ -1,32 +1,46 @@
package nl.andrewl.concord_core.msg.types;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
+import java.util.UUID;
+
+import static nl.andrewl.concord_core.msg.MessageUtils.*;
/**
* This message is sent from the server to the client after the server accepts
* the client's identification and registers the client in the server.
*/
@Data
+@NoArgsConstructor
+@AllArgsConstructor
public class ServerWelcome implements Message {
- private long clientId;
+ private UUID clientId;
+ private UUID currentChannelId;
+ private ServerMetaData metaData;
@Override
public int getByteCount() {
- return Long.BYTES;
+ return 2 * UUID_BYTES + this.metaData.getByteCount();
}
@Override
public void write(DataOutputStream o) throws IOException {
- o.writeLong(this.clientId);
+ writeUUID(this.clientId, o);
+ writeUUID(this.currentChannelId, o);
+ this.metaData.write(o);
}
@Override
public void read(DataInputStream i) throws IOException {
- this.clientId = i.readLong();
+ this.clientId = readUUID(i);
+ this.currentChannelId = readUUID(i);
+ this.metaData = new ServerMetaData();
+ this.metaData.read(i);
}
}
diff --git a/server/pom.xml b/server/pom.xml
index 80f9012..5b86686 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -25,23 +25,4 @@
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.1
-
-
-
- org.projectlombok
- lombok
- 1.18.20
-
-
-
-
-
-
\ No newline at end of file
diff --git a/server/src/main/java/nl/andrewl/concord_server/Channel.java b/server/src/main/java/nl/andrewl/concord_server/Channel.java
new file mode 100644
index 0000000..a79a9d3
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/Channel.java
@@ -0,0 +1,72 @@
+package nl.andrewl.concord_server;
+
+import lombok.Getter;
+import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_core.msg.Serializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Represents a single communication area in which messages are sent by clients
+ * and received by all connected clients.
+ */
+@Getter
+public class Channel {
+ private final ConcordServer server;
+ private UUID id;
+ private String name;
+
+ private final Set connectedClients;
+
+ public Channel(ConcordServer server, UUID id, String name) {
+ this.server = server;
+ this.id = id;
+ this.name = name;
+ this.connectedClients = ConcurrentHashMap.newKeySet();
+ }
+
+ public void addClient(ClientThread clientThread) {
+ this.connectedClients.add(clientThread);
+ }
+
+ public void removeClient(ClientThread clientThread) {
+ this.connectedClients.remove(clientThread);
+ }
+
+ /**
+ * Sends a message to all clients that are currently connected to this
+ * channel.
+ * @param msg The message to send.
+ * @throws IOException If an error occurs.
+ */
+ public void sendMessage(Message msg) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1);
+ Serializer.writeMessage(msg, baos);
+ byte[] data = baos.toByteArray();
+ for (var client : this.connectedClients) {
+ client.sendToClient(data);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Channel channel)) return false;
+ return name.equals(channel.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public String toString() {
+ return this.name;
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java b/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java
new file mode 100644
index 0000000..4e247c0
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/ChannelManager.java
@@ -0,0 +1,54 @@
+package nl.andrewl.concord_server;
+
+import nl.andrewl.concord_core.msg.types.MoveToChannel;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ChannelManager {
+ private final ConcordServer server;
+ private final Map channelNameMap;
+ private final Map channelIdMap;
+
+ public ChannelManager(ConcordServer server) {
+ this.server = server;
+ this.channelNameMap = new ConcurrentHashMap<>();
+ this.channelIdMap = new ConcurrentHashMap<>();
+ Channel general = new Channel(server, server.getIdProvider().newId(), "general");
+ Channel memes = new Channel(server, server.getIdProvider().newId(), "memes");
+ this.addChannel(general);
+ this.addChannel(memes);
+ }
+
+ public Set getChannels() {
+ return Set.copyOf(this.channelIdMap.values());
+ }
+
+ public void addChannel(Channel channel) {
+ this.channelNameMap.put(channel.getName(), channel);
+ this.channelIdMap.put(channel.getId(), channel);
+ }
+
+ public void removeChannel(Channel channel) {
+ this.channelNameMap.remove(channel.getName());
+ this.channelIdMap.remove(channel.getId());
+ }
+
+ public Optional getChannelByName(String name) {
+ return Optional.ofNullable(this.channelNameMap.get(name));
+ }
+
+ public Optional getChannelById(UUID id) {
+ return Optional.ofNullable(this.channelIdMap.get(id));
+ }
+
+ public void moveToChannel(ClientThread client, Channel channel) {
+ if (client.getCurrentChannel() != null) {
+ client.getCurrentChannel().removeClient(client);
+ }
+ channel.addClient(client);
+ client.setCurrentChannel(channel);
+ client.sendToClient(new MoveToChannel(channel.getId()));
+ System.out.println("Moved client " + client.getClientNickname() + " to channel " + channel.getName());
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/ChatThread.java b/server/src/main/java/nl/andrewl/concord_server/ChatThread.java
new file mode 100644
index 0000000..7aeed1d
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/ChatThread.java
@@ -0,0 +1,4 @@
+package nl.andrewl.concord_server;
+
+public class ChatThread {
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java
index d4d99f5..988d17c 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ClientThread.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ClientThread.java
@@ -1,11 +1,10 @@
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.Chat;
-import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
import nl.andrewl.concord_core.msg.types.Identification;
import nl.andrewl.concord_core.msg.types.ServerWelcome;
@@ -13,6 +12,7 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
+import java.util.UUID;
/**
* This thread is responsible for handling the connection to a single client of
@@ -26,10 +26,16 @@ public class ClientThread extends Thread {
private final ConcordServer server;
- private Long clientId = null;
+ private UUID clientId = null;
@Getter
private String clientNickname = null;
+ @Getter
+ @Setter
+ private Channel currentChannel;
+
+ private volatile boolean running;
+
public ClientThread(Socket socket, ConcordServer server) throws IOException {
this.socket = socket;
this.server = server;
@@ -54,42 +60,60 @@ public class ClientThread extends Thread {
}
}
+ public void shutdown() {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ this.running = false;
+ }
+
@Override
public void run() {
+ this.running = true;
if (!identifyClient()) {
log.warning("Could not identify the client; aborting connection.");
- return;
+ this.running = false;
}
- while (true) {
+ while (this.running) {
try {
var msg = Serializer.readMessage(this.in);
- if (msg instanceof Chat chat) {
- this.server.handleChat(chat);
- } else if (msg instanceof ChatHistoryRequest historyRequest) {
- this.server.handleHistoryRequest(historyRequest, this);
- }
+ this.server.getEventManager().handle(msg, this);
} catch (IOException e) {
log.info("Client disconnected: " + e.getMessage());
- if (this.clientId != null) {
- this.server.deregisterClient(this.clientId);
- }
- break;
+ this.running = false;
}
}
+
+ if (this.clientId != null) {
+ this.server.deregisterClient(this.clientId);
+ }
+ try {
+ if (!this.socket.isClosed()) {
+ this.socket.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
+ /**
+ * Initial method that attempts to obtain identification information from a
+ * newly-connected client. It is the intent that we should close the socket
+ * if the client is not able to identify itself.
+ * @return True if we were able to obtain identification from the client, or
+ * false otherwise.
+ */
private boolean identifyClient() {
int attempts = 0;
while (attempts < 5) {
try {
var msg = Serializer.readMessage(this.in);
if (msg instanceof Identification id) {
- this.clientId = this.server.registerClient(this);
this.clientNickname = id.getNickname();
- var reply = new ServerWelcome();
- reply.setClientId(this.clientId);
- Serializer.writeMessage(reply, this.out);
+ this.clientId = this.server.registerClient(id, this);
return true;
}
} catch (IOException e) {
diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
index e10936a..13c13b6 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
@@ -1,85 +1,100 @@
package nl.andrewl.concord_server;
+import lombok.Getter;
import lombok.extern.java.Log;
-import nl.andrewl.concord_core.msg.Serializer;
-import nl.andrewl.concord_core.msg.types.Chat;
-import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
-import org.dizitart.no2.Document;
+import nl.andrewl.concord_core.msg.types.Identification;
+import nl.andrewl.concord_core.msg.types.ServerMetaData;
+import nl.andrewl.concord_core.msg.types.ServerWelcome;
import org.dizitart.no2.Nitrite;
-import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
-import java.security.SecureRandom;
+import java.util.Comparator;
import java.util.Map;
-import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
@Log
public class ConcordServer implements Runnable {
- private final Map clients = new ConcurrentHashMap<>(32);
+ private final Map clients;
private final int port;
- private final Random random;
+ @Getter
+ private final IdProvider idProvider;
+ @Getter
private final Nitrite db;
+ private volatile boolean running;
+ @Getter
+ private final ExecutorService executorService;
+ @Getter
+ private final EventManager eventManager;
+ @Getter
+ private final ChannelManager channelManager;
public ConcordServer(int port) {
this.port = port;
- this.random = new SecureRandom();
+ this.idProvider = new UUIDProvider();
this.db = Nitrite.builder()
.filePath("concord-server.db")
.openOrCreate();
+ this.clients = new ConcurrentHashMap<>(32);
+
+ this.executorService = Executors.newCachedThreadPool();
+ this.eventManager = new EventManager(this);
+ this.channelManager = new ChannelManager(this);
}
- public long registerClient(ClientThread clientThread) {
- long id = this.random.nextLong();
- log.info("Registering new client " + clientThread.getClientNickname() + " with id " + id);
+ /**
+ * Registers a new client as connected to the server. This is done once the
+ * client thread has received the correct identification information from
+ * the client. The server will register the client in its global set of
+ * connected clients, and it will immediately move the client to the default
+ * channel.
+ * @param identification The client's identification data.
+ * @param clientThread The client manager thread.
+ * @return The id of the client.
+ */
+ public UUID registerClient(Identification identification, ClientThread clientThread) {
+ var id = this.idProvider.newId();
+ log.info("Registering new client " + identification.getNickname() + " with id " + id);
this.clients.put(id, clientThread);
+ // Send a welcome reply containing all the initial server info the client needs.
+ ServerMetaData metaData = new ServerMetaData(
+ "Testing Server",
+ this.channelManager.getChannels().stream()
+ .map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
+ .sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
+ .collect(Collectors.toList())
+ );
+ var defaultChannel = this.channelManager.getChannelByName("general").orElseThrow();
+ defaultChannel.addClient(clientThread);
+ clientThread.setCurrentChannel(defaultChannel);
+ clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData));
+
return id;
}
- public void deregisterClient(long clientId) {
- this.clients.remove(clientId);
- }
-
- public void handleChat(Chat chat) {
- var collection = db.getCollection("channel-TEST");
- long messageId = this.random.nextLong();
- Document doc = Document.createDocument(Long.toHexString(messageId), "message")
- .put("senderId", Long.toHexString(chat.getSenderId()))
- .put("senderNickname", chat.getSenderNickname())
- .put("timestamp", chat.getTimestamp())
- .put("message", chat.getMessage());
- collection.insert(doc);
- db.commit();
- System.out.println(chat.getSenderNickname() + ": " + chat.getMessage());
- ByteArrayOutputStream baos = new ByteArrayOutputStream(chat.getByteCount());
- try {
- Serializer.writeMessage(chat, new DataOutputStream(baos));
- } catch (IOException e) {
- e.printStackTrace();
- return;
+ public void deregisterClient(UUID clientId) {
+ var client = this.clients.remove(clientId);
+ if (client != null) {
+ client.getCurrentChannel().removeClient(client);
+ client.shutdown();
}
- byte[] data = baos.toByteArray();
- for (var client : clients.values()) {
- client.sendToClient(data);
- }
- }
-
- public void handleHistoryRequest(ChatHistoryRequest request, ClientThread clientThread) {
-
}
@Override
public void run() {
+ this.running = true;
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(this.port);
log.info("Opened server on port " + this.port);
- while (true) {
+ while (this.running) {
Socket socket = serverSocket.accept();
- log.info("Accepted new socket connection.");
+ log.info("Accepted new socket connection from " + socket.getInetAddress().getHostAddress());
ClientThread clientThread = new ClientThread(socket, this);
clientThread.start();
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/EventManager.java b/server/src/main/java/nl/andrewl/concord_server/EventManager.java
new file mode 100644
index 0000000..cdc1deb
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/EventManager.java
@@ -0,0 +1,37 @@
+package nl.andrewl.concord_server;
+
+import lombok.extern.java.Log;
+import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_core.msg.types.MoveToChannel;
+import nl.andrewl.concord_server.event.ChannelMoveHandler;
+import nl.andrewl.concord_server.event.ChatHandler;
+import nl.andrewl.concord_server.event.MessageHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Log
+public class EventManager {
+ private final Map, MessageHandler>> messageHandlers;
+ private final ConcordServer server;
+
+ public EventManager(ConcordServer server) {
+ this.server = server;
+ this.messageHandlers = new HashMap<>();
+ this.messageHandlers.put(Chat.class, new ChatHandler());
+ this.messageHandlers.put(MoveToChannel.class, new ChannelMoveHandler());
+ }
+
+ @SuppressWarnings("unchecked")
+ public void handle(T message, ClientThread client) {
+ MessageHandler handler = (MessageHandler) this.messageHandlers.get(message.getClass());
+ if (handler != null) {
+ try {
+ handler.handle(message, client, this.server);
+ } catch (Exception e) {
+ log.warning("Exception occurred while handling message: " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/IdProvider.java b/server/src/main/java/nl/andrewl/concord_server/IdProvider.java
new file mode 100644
index 0000000..c3318c9
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/IdProvider.java
@@ -0,0 +1,7 @@
+package nl.andrewl.concord_server;
+
+import java.util.UUID;
+
+public interface IdProvider {
+ UUID newId();
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/UUIDProvider.java b/server/src/main/java/nl/andrewl/concord_server/UUIDProvider.java
new file mode 100644
index 0000000..bea51ca
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/UUIDProvider.java
@@ -0,0 +1,10 @@
+package nl.andrewl.concord_server;
+
+import java.util.UUID;
+
+public class UUIDProvider implements IdProvider {
+ @Override
+ public UUID newId() {
+ return UUID.randomUUID();
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java
new file mode 100644
index 0000000..6688985
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java
@@ -0,0 +1,16 @@
+package nl.andrewl.concord_server.event;
+
+import nl.andrewl.concord_core.msg.types.MoveToChannel;
+import nl.andrewl.concord_server.ClientThread;
+import nl.andrewl.concord_server.ConcordServer;
+
+/**
+ * Handles client requests to move to another channel.
+ */
+public class ChannelMoveHandler implements MessageHandler {
+ @Override
+ public void handle(MoveToChannel msg, ClientThread client, ConcordServer server) {
+ var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
+ optionalChannel.ifPresent(channel -> server.getChannelManager().moveToChannel(client, channel));
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java
new file mode 100644
index 0000000..7f395ad
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java
@@ -0,0 +1,29 @@
+package nl.andrewl.concord_server.event;
+
+import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_server.ClientThread;
+import nl.andrewl.concord_server.ConcordServer;
+import org.dizitart.no2.Document;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class ChatHandler implements MessageHandler {
+ @Override
+ public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException {
+ server.getExecutorService().submit(() -> {
+ var collection = server.getDb().getCollection("channel-" + client.getCurrentChannel().getId());
+ var messageId = server.getIdProvider().newId();
+ Document doc = new Document(Map.of(
+ "_id", messageId,
+ "senderId", msg.getSenderId(),
+ "senderNickname", msg.getSenderNickname(),
+ "timestamp", msg.getTimestamp(),
+ "message", msg.getMessage()
+ ));
+ collection.insert(doc);
+ });
+ System.out.printf("#%s | %s: %s\n", client.getCurrentChannel(), client.getClientNickname(), msg.getMessage());
+ client.getCurrentChannel().sendMessage(msg);
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java b/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java
new file mode 100644
index 0000000..b4385bc
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_server.event;
+
+import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_server.ClientThread;
+import nl.andrewl.concord_server.ConcordServer;
+
+public interface EventListener {
+ default void chatMessageReceived(ConcordServer server, Chat chat, ClientThread client) {}
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java
new file mode 100644
index 0000000..aa5df2b
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_server.event;
+
+import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_server.ClientThread;
+import nl.andrewl.concord_server.ConcordServer;
+
+public interface MessageHandler {
+ void handle(T msg, ClientThread client, ConcordServer server) throws Exception;
+}