Implemented storing user messages in collections, improved network organization.

This commit is contained in:
Andrew Lalis 2021-08-22 22:58:29 +02:00
parent cc5c90fd54
commit fcfea0f70f
21 changed files with 419 additions and 57 deletions

View File

@ -11,12 +11,8 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import nl.andrewl.concord_client.gui.MainWindow; import nl.andrewl.concord_client.gui.MainWindow;
import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_core.msg.Message;
import nl.andrewl.concord_core.msg.MessageUtils;
import nl.andrewl.concord_core.msg.Serializer; import nl.andrewl.concord_core.msg.Serializer;
import nl.andrewl.concord_core.msg.types.Chat; import nl.andrewl.concord_core.msg.types.*;
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 java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
@ -28,7 +24,6 @@ import java.util.Set;
import java.util.UUID; 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;
@ -53,6 +48,10 @@ public class ConcordClient implements Runnable {
this.id = welcome.getClientId(); this.id = welcome.getClientId();
this.currentChannelId = welcome.getCurrentChannelId(); this.currentChannelId = welcome.getCurrentChannelId();
this.serverMetaData = welcome.getMetaData(); this.serverMetaData = 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, ""));
} 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.");
} }

View File

@ -10,11 +10,17 @@ import nl.andrewl.concord_client.ConcordClient;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/**
* This panel occupies the center of the interface, and displays the list of
* recent messages, along with an input text box for the user to type messages
* into.
*/
public class ChannelChatBox extends Panel { public class ChannelChatBox extends Panel {
private final ConcordClient client; private final ConcordClient client;
private Border chatBorder; private Border chatBorder;
@Getter @Getter
private final ChatList chatList; private final ChatList chatList;
@Getter
private final TextBox inputTextBox; private final TextBox inputTextBox;
public ChannelChatBox(ConcordClient client, Window window) { public ChannelChatBox(ConcordClient client, Window window) {
super(new BorderLayout()); super(new BorderLayout());

View File

@ -1,9 +1,6 @@
package nl.andrewl.concord_client.gui; package nl.andrewl.concord_client.gui;
import com.googlecode.lanterna.gui2.Button; import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.Direction;
import com.googlecode.lanterna.gui2.LinearLayout;
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.MoveToChannel; import nl.andrewl.concord_core.msg.types.MoveToChannel;
@ -11,7 +8,6 @@ import java.io.IOException;
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) {
super(new LinearLayout(Direction.VERTICAL)); super(new LinearLayout(Direction.VERTICAL));
this.client = client; this.client = client;
@ -25,11 +21,12 @@ public class ChannelList extends Panel {
name = "*" + name; name = "*" + name;
} }
Button b = new Button(name, () -> { Button b = new Button(name, () -> {
System.out.println("Sending request to go to channel " + channel.getName()); if (!client.getCurrentChannelId().equals(channel.getId())) {
try { try {
client.sendMessage(new MoveToChannel(channel.getId())); client.sendMessage(new MoveToChannel(channel.getId()));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
}
} }
}); });
this.addComponent(b, LinearLayout.createLayoutData(LinearLayout.Alignment.End)); this.addComponent(b, LinearLayout.createLayoutData(LinearLayout.Alignment.End));

View File

@ -5,8 +5,7 @@ import lombok.Getter;
import nl.andrewl.concord_client.ClientMessageListener; 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_core.msg.Message;
import nl.andrewl.concord_core.msg.types.Chat; import nl.andrewl.concord_core.msg.types.*;
import nl.andrewl.concord_core.msg.types.MoveToChannel;
import java.io.IOException; import java.io.IOException;
@ -24,16 +23,16 @@ public class ChatPanel extends Panel implements ClientMessageListener {
private final UserList userList; private final UserList userList;
private final ConcordClient client; private final ConcordClient client;
private final TextGUIThread guiThread;
public ChatPanel(ConcordClient client, Window window) { public ChatPanel(ConcordClient client, Window window) {
super(new BorderLayout()); super(new BorderLayout());
this.guiThread = window.getTextGUI().getGUIThread();
this.client = client; 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();
this.userList = new UserList(); this.userList = new UserList(client);
this.userList.addItem("andrew");
this.userList.addItem("tester");
Border b; Border b;
b = Borders.doubleLine("Channels"); b = Borders.doubleLine("Channels");
@ -53,9 +52,25 @@ public class ChatPanel extends Panel implements ClientMessageListener {
this.channelChatBox.getChatList().addItem(chat); this.channelChatBox.getChatList().addItem(chat);
} else if (message instanceof MoveToChannel moveToChannel) { } else if (message instanceof MoveToChannel moveToChannel) {
client.setCurrentChannelId(moveToChannel.getChannelId()); client.setCurrentChannelId(moveToChannel.getChannelId());
this.channelList.setChannels(); try {
this.channelChatBox.getChatList().clearItems(); client.sendMessage(new ChatHistoryRequest(moveToChannel.getChannelId(), ChatHistoryRequest.Source.CHANNEL, ""));
this.channelChatBox.refreshBorder(); 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());
} }
} }
} }

View File

@ -1,6 +1,27 @@
package nl.andrewl.concord_client.gui; package nl.andrewl.concord_client.gui;
import com.googlecode.lanterna.gui2.AbstractListBox; import com.googlecode.lanterna.gui2.Button;
import com.googlecode.lanterna.gui2.Direction;
import com.googlecode.lanterna.gui2.LinearLayout;
import com.googlecode.lanterna.gui2.Panel;
import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
public class UserList extends AbstractListBox<String, UserList> { public class UserList extends Panel {
private final ConcordClient client;
public UserList(ConcordClient client) {
super(new LinearLayout(Direction.VERTICAL));
this.client = client;
}
public void updateUsers(ChannelUsersResponse usersResponse) {
this.removeAllComponents();
for (var user : usersResponse.getUsers()) {
Button b = new Button(user.getName(), () -> {
System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
});
this.addComponent(b);
}
}
} }

View File

@ -19,6 +19,8 @@ public class Serializer {
registerType(3, MoveToChannel.class); registerType(3, MoveToChannel.class);
registerType(4, ChatHistoryRequest.class); registerType(4, ChatHistoryRequest.class);
registerType(5, ChatHistoryResponse.class); registerType(5, ChatHistoryResponse.class);
registerType(6, ChannelUsersRequest.class);
registerType(7, ChannelUsersResponse.class);
} }
private static void registerType(int id, Class<? extends Message> clazz) { private static void registerType(int id, Class<? extends Message> clazz) {

View File

@ -0,0 +1,35 @@
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.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChannelUsersRequest implements Message {
private UUID 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);
}
}

View File

@ -0,0 +1,65 @@
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 ChannelUsersResponse implements Message {
private List<UserData> users;
@Override
public int getByteCount() {
return getByteSize(this.users);
}
@Override
public void write(DataOutputStream o) throws IOException {
writeList(this.users, o);
}
@Override
public void read(DataInputStream i) throws IOException {
try {
this.users = readList(UserData.class, i);
} catch (ReflectiveOperationException e) {
throw new IOException(e);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class UserData implements Message {
private UUID id;
private String name;
@Override
public int getByteCount() {
return UUID_BYTES + getByteSize(this.name);
}
@Override
public void write(DataOutputStream o) throws IOException {
writeUUID(this.id, o);
writeString(this.name, o);
}
@Override
public void read(DataInputStream i) throws IOException {
this.id = readUUID(i);
this.name = readString(i);
}
}
}

View File

@ -1,5 +1,6 @@
package nl.andrewl.concord_core.msg.types; package nl.andrewl.concord_core.msg.types;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_core.msg.Message;
@ -46,6 +47,7 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
public class ChatHistoryRequest implements Message { public class ChatHistoryRequest implements Message {
public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE} public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}

View File

@ -1,5 +1,6 @@
package nl.andrewl.concord_core.msg.types; package nl.andrewl.concord_core.msg.types;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_core.msg.Message;
@ -17,6 +18,7 @@ import java.util.UUID;
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
public class ChatHistoryResponse implements Message { public class ChatHistoryResponse implements Message {
private UUID sourceId; private UUID sourceId;
private ChatHistoryRequest.Source sourceType; private ChatHistoryRequest.Source sourceType;

View File

@ -24,5 +24,24 @@
<version>3.4.3</version> <version>3.4.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.4</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,9 +1,14 @@
module concord_server { module concord_server {
requires nitrite; requires nitrite;
requires static lombok; requires static lombok;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.annotation;
requires java.base; requires java.base;
requires java.logging; requires java.logging;
requires concord_core; requires concord_core;
opens nl.andrewl.concord_server.config to com.fasterxml.jackson.databind;
} }

View File

@ -3,12 +3,12 @@ package nl.andrewl.concord_server;
import lombok.Getter; import lombok.Getter;
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.ChannelUsersResponse;
import org.dizitart.no2.NitriteCollection;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.*;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
@ -23,19 +23,32 @@ public class Channel {
private final Set<ClientThread> connectedClients; private final Set<ClientThread> connectedClients;
public Channel(ConcordServer server, UUID id, String name) { private final NitriteCollection messageCollection;
public Channel(ConcordServer server, UUID id, String name, NitriteCollection messageCollection) {
this.server = server; this.server = server;
this.id = id; this.id = id;
this.name = name; this.name = name;
this.connectedClients = ConcurrentHashMap.newKeySet(); this.connectedClients = ConcurrentHashMap.newKeySet();
this.messageCollection = messageCollection;
} }
public void addClient(ClientThread clientThread) { public void addClient(ClientThread clientThread) {
this.connectedClients.add(clientThread); this.connectedClients.add(clientThread);
try {
this.sendMessage(new ChannelUsersResponse(this.getUserData()));
} catch (IOException e) {
e.printStackTrace();
}
} }
public void removeClient(ClientThread clientThread) { public void removeClient(ClientThread clientThread) {
this.connectedClients.remove(clientThread); this.connectedClients.remove(clientThread);
try {
this.sendMessage(new ChannelUsersResponse(this.getUserData()));
} catch (IOException e) {
e.printStackTrace();
}
} }
/** /**
@ -53,6 +66,15 @@ public class Channel {
} }
} }
public List<ChannelUsersResponse.UserData> getUserData() {
List<ChannelUsersResponse.UserData> users = new ArrayList<>();
for (var clientThread : this.getConnectedClients()) {
users.add(new ChannelUsersResponse.UserData(clientThread.getClientId(), clientThread.getClientNickname()));
}
users.sort(Comparator.comparing(ChannelUsersResponse.UserData::getName));
return users;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -2,7 +2,10 @@ package nl.andrewl.concord_server;
import nl.andrewl.concord_core.msg.types.MoveToChannel; import nl.andrewl.concord_core.msg.types.MoveToChannel;
import java.util.*; import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class ChannelManager { public class ChannelManager {
@ -14,10 +17,6 @@ public class ChannelManager {
this.server = server; this.server = server;
this.channelNameMap = new ConcurrentHashMap<>(); this.channelNameMap = new ConcurrentHashMap<>();
this.channelIdMap = 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<Channel> getChannels() { public Set<Channel> getChannels() {
@ -44,7 +43,8 @@ public class ChannelManager {
public void moveToChannel(ClientThread client, Channel channel) { public void moveToChannel(ClientThread client, Channel channel) {
if (client.getCurrentChannel() != null) { if (client.getCurrentChannel() != null) {
client.getCurrentChannel().removeClient(client); var previousChannel = client.getCurrentChannel();
previousChannel.removeClient(client);
} }
channel.addClient(client); channel.addClient(client);
client.setCurrentChannel(channel); client.setCurrentChannel(channel);

View File

@ -6,7 +6,6 @@ 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;
import nl.andrewl.concord_core.msg.types.ServerWelcome;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
@ -16,7 +15,7 @@ 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. * a server. The client thread acts as the server's representation of a client.
*/ */
@Log @Log
public class ClientThread extends Thread { public class ClientThread extends Thread {
@ -26,8 +25,11 @@ public class ClientThread extends Thread {
private final ConcordServer server; private final ConcordServer server;
@Getter
@Setter
private UUID clientId = null; private UUID clientId = null;
@Getter @Getter
@Setter
private String clientNickname = null; private String clientNickname = null;
@Getter @Getter
@ -43,7 +45,7 @@ public class ClientThread extends Thread {
this.out = new DataOutputStream(socket.getOutputStream()); this.out = new DataOutputStream(socket.getOutputStream());
} }
public void sendToClient(Message message) { public synchronized void sendToClient(Message message) {
try { try {
Serializer.writeMessage(message, this.out); Serializer.writeMessage(message, this.out);
} catch (IOException e) { } catch (IOException e) {
@ -51,7 +53,7 @@ public class ClientThread extends Thread {
} }
} }
public void sendToClient(byte[] bytes) { public synchronized void sendToClient(byte[] bytes) {
try { try {
this.out.write(bytes); this.out.write(bytes);
this.out.flush(); this.out.flush();
@ -112,8 +114,7 @@ public class ClientThread extends Thread {
try { try {
var msg = Serializer.readMessage(this.in); var msg = Serializer.readMessage(this.in);
if (msg instanceof Identification id) { if (msg instanceof Identification id) {
this.clientNickname = id.getNickname(); this.server.registerClient(id, this);
this.clientId = this.server.registerClient(id, this);
return true; return true;
} }
} catch (IOException e) { } catch (IOException e) {

View File

@ -5,11 +5,15 @@ import lombok.extern.java.Log;
import nl.andrewl.concord_core.msg.types.Identification; import nl.andrewl.concord_core.msg.types.Identification;
import nl.andrewl.concord_core.msg.types.ServerMetaData; import nl.andrewl.concord_core.msg.types.ServerMetaData;
import nl.andrewl.concord_core.msg.types.ServerWelcome; import nl.andrewl.concord_core.msg.types.ServerWelcome;
import nl.andrewl.concord_server.config.ServerConfig;
import org.dizitart.no2.IndexOptions;
import org.dizitart.no2.IndexType;
import org.dizitart.no2.Nitrite; import org.dizitart.no2.Nitrite;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.file.Path;
import java.util.Comparator; import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -22,6 +26,7 @@ import java.util.stream.Collectors;
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 final int port;
private final String name;
@Getter @Getter
private final IdProvider idProvider; private final IdProvider idProvider;
@Getter @Getter
@ -34,9 +39,11 @@ public class ConcordServer implements Runnable {
@Getter @Getter
private final ChannelManager channelManager; private final ChannelManager channelManager;
public ConcordServer(int port) { public ConcordServer() {
this.port = port;
this.idProvider = new UUIDProvider(); this.idProvider = new UUIDProvider();
ServerConfig 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();
@ -45,6 +52,33 @@ public class ConcordServer implements Runnable {
this.executorService = Executors.newCachedThreadPool(); this.executorService = Executors.newCachedThreadPool();
this.eventManager = new EventManager(this); this.eventManager = new EventManager(this);
this.channelManager = new ChannelManager(this); this.channelManager = new ChannelManager(this);
for (var channelConfig : config.channels()) {
this.channelManager.addChannel(new Channel(
this,
UUID.fromString(channelConfig.id()),
channelConfig.name(),
this.db.getCollection("channel-" + channelConfig.id())
));
}
this.initDatabase();
}
private void initDatabase() {
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());
col.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique));
}
if (!col.hasIndex("senderNickname")) {
log.info("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());
col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext));
}
}
} }
/** /**
@ -55,28 +89,34 @@ public class ConcordServer implements Runnable {
* channel. * channel.
* @param identification The client's identification data. * @param identification The client's identification data.
* @param clientThread The client manager thread. * @param clientThread The client manager thread.
* @return The id of the client.
*/ */
public UUID 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); log.info("Registering new client " + identification.getNickname() + " with id " + id);
this.clients.put(id, clientThread); 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. // Send a welcome reply containing all the initial server info the client needs.
ServerMetaData metaData = new ServerMetaData( ServerMetaData metaData = new ServerMetaData(
"Testing Server", this.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))
.collect(Collectors.toList()) .collect(Collectors.toList())
); );
// Immediately add the client to the default channel and send the initial welcome message.
var defaultChannel = this.channelManager.getChannelByName("general").orElseThrow(); var defaultChannel = this.channelManager.getChannelByName("general").orElseThrow();
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData));
// 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);
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData));
return id;
} }
/**
* De-registers a client from the server, removing them from any channel
* they're currently in.
* @param clientId The id of the client to remove.
*/
public void deregisterClient(UUID clientId) { public void deregisterClient(UUID clientId) {
var client = this.clients.remove(clientId); var client = this.clients.remove(clientId);
if (client != null) { if (client != null) {
@ -104,7 +144,7 @@ public class ConcordServer implements Runnable {
} }
public static void main(String[] args) { public static void main(String[] args) {
var server = new ConcordServer(8123); var server = new ConcordServer();
server.run(); server.run();
} }
} }

View File

@ -2,15 +2,19 @@ package nl.andrewl.concord_server;
import lombok.extern.java.Log; 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.types.ChannelUsersRequest;
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.MoveToChannel; import nl.andrewl.concord_core.msg.types.MoveToChannel;
import nl.andrewl.concord_server.event.ChannelMoveHandler; import nl.andrewl.concord_server.event.*;
import nl.andrewl.concord_server.event.ChatHandler;
import nl.andrewl.concord_server.event.MessageHandler;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* The event manager is responsible for the server's ability to respond to
* various client requests.
*/
@Log @Log
public class EventManager { public class EventManager {
private final Map<Class<? extends Message>, MessageHandler<?>> messageHandlers; private final Map<Class<? extends Message>, MessageHandler<?>> messageHandlers;
@ -21,8 +25,25 @@ public class EventManager {
this.messageHandlers = new HashMap<>(); this.messageHandlers = new HashMap<>();
this.messageHandlers.put(Chat.class, new ChatHandler()); this.messageHandlers.put(Chat.class, new ChatHandler());
this.messageHandlers.put(MoveToChannel.class, new ChannelMoveHandler()); this.messageHandlers.put(MoveToChannel.class, new ChannelMoveHandler());
this.messageHandlers.put(ChannelUsersRequest.class, new ChannelUsersRequestHandler());
this.messageHandlers.put(ChatHistoryRequest.class, new ChatHistoryRequestHandler());
} }
/**
* Handles a new message that was sent from a client. Tries to find an
* appropriate handler for the message, and if one is found, calls the
* {@link MessageHandler#handle(Message, ClientThread, ConcordServer)}
* method on it.
* <p>
* Note that it is expected that client threads will invoke this method
* during their {@link ClientThread#run()} method, so concurrent
* invocation is expected.
* </p>
* @param message The message that was sent by a client.
* @param client The client thread that is used for communicating with the
* client.
* @param <T> The type of message.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T extends Message> void handle(T message, ClientThread client) { public <T extends Message> void handle(T message, ClientThread client) {
MessageHandler<T> handler = (MessageHandler<T>) this.messageHandlers.get(message.getClass()); MessageHandler<T> handler = (MessageHandler<T>) this.messageHandlers.get(message.getClass());
@ -30,6 +51,7 @@ public class EventManager {
try { try {
handler.handle(message, client, this.server); handler.handle(message, client, this.server);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
log.warning("Exception occurred while handling message: " + e.getMessage()); log.warning("Exception occurred while handling message: " + e.getMessage());
} }
} }

View File

@ -0,0 +1,52 @@
package nl.andrewl.concord_server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.java.Log;
import nl.andrewl.concord_server.IdProvider;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Log
public record ServerConfig(
String name,
int port,
ChannelConfig[] channels
) {
public static record ChannelConfig (
String id,
String name,
String description
) {}
public static ServerConfig loadOrCreate(Path filePath, IdProvider idProvider) {
ObjectMapper mapper = new ObjectMapper();
ServerConfig config;
if (Files.notExists(filePath)) {
config = new ServerConfig(
"My Concord Server",
8123,
new ServerConfig.ChannelConfig[]{
new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")
}
);
try (var out = Files.newOutputStream(filePath)) {
mapper.writerWithDefaultPrettyPrinter().writeValue(out, config);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
log.info(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);
}
return config;
}
}

View File

@ -0,0 +1,17 @@
package nl.andrewl.concord_server.event;
import nl.andrewl.concord_core.msg.types.ChannelUsersRequest;
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
import nl.andrewl.concord_server.ClientThread;
import nl.andrewl.concord_server.ConcordServer;
public class ChannelUsersRequestHandler implements MessageHandler<ChannelUsersRequest> {
@Override
public void handle(ChannelUsersRequest msg, ClientThread client, ConcordServer server) throws Exception {
var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
if (optionalChannel.isPresent()) {
var channel = optionalChannel.get();
client.sendToClient(new ChannelUsersResponse(channel.getUserData()));
}
}
}

View File

@ -12,10 +12,8 @@ public class ChatHandler implements MessageHandler<Chat> {
@Override @Override
public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException { public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException {
server.getExecutorService().submit(() -> { server.getExecutorService().submit(() -> {
var collection = server.getDb().getCollection("channel-" + client.getCurrentChannel().getId()); var collection = client.getCurrentChannel().getMessageCollection();
var messageId = server.getIdProvider().newId();
Document doc = new Document(Map.of( Document doc = new Document(Map.of(
"_id", messageId,
"senderId", msg.getSenderId(), "senderId", msg.getSenderId(),
"senderNickname", msg.getSenderNickname(), "senderNickname", msg.getSenderNickname(),
"timestamp", msg.getTimestamp(), "timestamp", msg.getTimestamp(),

View File

@ -0,0 +1,42 @@
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.ClientThread;
import nl.andrewl.concord_server.ConcordServer;
import org.dizitart.no2.Document;
import org.dizitart.no2.FindOptions;
import org.dizitart.no2.SortOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequest> {
@Override
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) throws Exception {
var optionalChannel = server.getChannelManager().getChannelById(msg.getSourceId());
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.Ascending)
.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)
));
}
col.close();
System.out.println(chats);
client.sendToClient(new ChatHistoryResponse(msg.getSourceId(), msg.getSourceType(), chats));
}
}
}