Added ability to add and remove channels.
This commit is contained in:
parent
b21137a767
commit
f159708fa2
|
@ -12,6 +12,7 @@ import nl.andrewl.concord_client.event.EventManager;
|
||||||
import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler;
|
import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler;
|
||||||
import nl.andrewl.concord_client.event.handlers.ChannelUsersResponseHandler;
|
import nl.andrewl.concord_client.event.handlers.ChannelUsersResponseHandler;
|
||||||
import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler;
|
import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler;
|
||||||
|
import nl.andrewl.concord_client.event.handlers.ServerMetaDataHandler;
|
||||||
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_client.model.ClientModel;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
@ -57,6 +58,7 @@ public class ConcordClient implements Runnable {
|
||||||
this.eventManager.addHandler(ChannelUsersResponse.class, new ChannelUsersResponseHandler());
|
this.eventManager.addHandler(ChannelUsersResponse.class, new ChannelUsersResponseHandler());
|
||||||
this.eventManager.addHandler(ChatHistoryResponse.class, new ChatHistoryResponseHandler());
|
this.eventManager.addHandler(ChatHistoryResponse.class, new ChatHistoryResponseHandler());
|
||||||
this.eventManager.addHandler(Chat.class, (msg, client) -> client.getModel().getChatHistory().addChat(msg));
|
this.eventManager.addHandler(Chat.class, (msg, client) -> client.getModel().getChatHistory().addChat(msg));
|
||||||
|
this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(Message message) throws IOException {
|
public void sendMessage(Message message) throws IOException {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package nl.andrewl.concord_client.event;
|
package nl.andrewl.concord_client.event;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
||||||
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -8,5 +9,7 @@ import java.util.UUID;
|
||||||
public interface ClientModelListener {
|
public interface ClientModelListener {
|
||||||
default void channelMoved(UUID oldChannelId, UUID newChannelId) {}
|
default void channelMoved(UUID oldChannelId, UUID newChannelId) {}
|
||||||
|
|
||||||
default void usersUpdated(List<ChannelUsersResponse.UserData> users) {}
|
default void usersUpdated(List<UserData> users) {}
|
||||||
|
|
||||||
|
default void serverMetaDataUpdated(ServerMetaData metaData) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ServerMetaData;
|
||||||
|
|
||||||
|
public class ServerMetaDataHandler implements MessageHandler<ServerMetaData> {
|
||||||
|
@Override
|
||||||
|
public void handle(ServerMetaData msg, ConcordClient client) {
|
||||||
|
client.getModel().setServerMetaData(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,8 @@ import com.googlecode.lanterna.gui2.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewl.concord_client.ConcordClient;
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
import nl.andrewl.concord_client.event.ClientModelListener;
|
import nl.andrewl.concord_client.event.ClientModelListener;
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
||||||
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -22,11 +23,8 @@ public class ServerPanel extends Panel implements ClientModelListener {
|
||||||
private final ChannelList channelList;
|
private final ChannelList channelList;
|
||||||
private final UserList userList;
|
private final UserList userList;
|
||||||
|
|
||||||
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.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();
|
||||||
|
@ -55,9 +53,16 @@ public class ServerPanel extends Panel implements ClientModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void usersUpdated(List<ChannelUsersResponse.UserData> users) {
|
public void usersUpdated(List<UserData> users) {
|
||||||
this.guiThread.invokeLater(() -> {
|
this.getTextGUI().getGUIThread().invokeLater(() -> {
|
||||||
this.userList.updateUsers(users);
|
this.userList.updateUsers(users);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serverMetaDataUpdated(ServerMetaData metaData) {
|
||||||
|
this.getTextGUI().getGUIThread().invokeLater(() -> {
|
||||||
|
this.channelList.setChannels();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.googlecode.lanterna.gui2.LinearLayout;
|
||||||
import com.googlecode.lanterna.gui2.Panel;
|
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 nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ public class UserList extends Panel {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUsers(List<ChannelUsersResponse.UserData> usersResponse) {
|
public void updateUsers(List<UserData> usersResponse) {
|
||||||
this.removeAllComponents();
|
this.removeAllComponents();
|
||||||
for (var user : usersResponse) {
|
for (var user : usersResponse) {
|
||||||
Button b = new Button(user.getName(), () -> {
|
Button b = new Button(user.getName(), () -> {
|
||||||
|
|
|
@ -2,8 +2,8 @@ package nl.andrewl.concord_client.model;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewl.concord_client.event.ClientModelListener;
|
import nl.andrewl.concord_client.event.ClientModelListener;
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
|
||||||
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
||||||
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -17,7 +17,7 @@ public class ClientModel {
|
||||||
private ServerMetaData serverMetaData;
|
private ServerMetaData serverMetaData;
|
||||||
|
|
||||||
private UUID currentChannelId;
|
private UUID currentChannelId;
|
||||||
private List<ChannelUsersResponse.UserData> knownUsers;
|
private List<UserData> knownUsers;
|
||||||
private final ChatHistory chatHistory;
|
private final ChatHistory chatHistory;
|
||||||
|
|
||||||
private final List<ClientModelListener> modelListeners;
|
private final List<ClientModelListener> modelListeners;
|
||||||
|
@ -38,11 +38,16 @@ public class ClientModel {
|
||||||
this.modelListeners.forEach(listener -> listener.channelMoved(oldId, newChannelId));
|
this.modelListeners.forEach(listener -> listener.channelMoved(oldId, newChannelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setKnownUsers(List<ChannelUsersResponse.UserData> users) {
|
public void setKnownUsers(List<UserData> users) {
|
||||||
this.knownUsers = users;
|
this.knownUsers = users;
|
||||||
this.modelListeners.forEach(listener -> listener.usersUpdated(this.knownUsers));
|
this.modelListeners.forEach(listener -> listener.usersUpdated(this.knownUsers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setServerMetaData(ServerMetaData metaData) {
|
||||||
|
this.serverMetaData = metaData;
|
||||||
|
this.modelListeners.forEach(listener -> listener.serverMetaDataUpdated(metaData));
|
||||||
|
}
|
||||||
|
|
||||||
public void addListener(ClientModelListener listener) {
|
public void addListener(ClientModelListener listener) {
|
||||||
this.modelListeners.add(listener);
|
this.modelListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ public class Serializer {
|
||||||
registerType(5, ChatHistoryResponse.class);
|
registerType(5, ChatHistoryResponse.class);
|
||||||
registerType(6, ChannelUsersRequest.class);
|
registerType(6, ChannelUsersRequest.class);
|
||||||
registerType(7, ChannelUsersResponse.class);
|
registerType(7, ChannelUsersResponse.class);
|
||||||
|
registerType(8, ServerMetaData.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void registerType(int id, Class<? extends Message> clazz) {
|
private static void registerType(int id, Class<? extends Message> clazz) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
|
@ -37,29 +36,4 @@ public class ChannelUsersResponse implements Message {
|
||||||
throw new IOException(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
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.*;
|
||||||
|
import static nl.andrewl.concord_core.msg.MessageUtils.readString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard set of user data that is used mainly as a component of other more
|
||||||
|
* complex messages.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,9 @@ 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 nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
||||||
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
import org.dizitart.no2.IndexOptions;
|
||||||
|
import org.dizitart.no2.IndexType;
|
||||||
import org.dizitart.no2.NitriteCollection;
|
import org.dizitart.no2.NitriteCollection;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -31,6 +34,29 @@ public class Channel {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.connectedClients = ConcurrentHashMap.newKeySet();
|
this.connectedClients = ConcurrentHashMap.newKeySet();
|
||||||
this.messageCollection = messageCollection;
|
this.messageCollection = messageCollection;
|
||||||
|
this.initCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCollection() {
|
||||||
|
if (!this.messageCollection.hasIndex("timestamp")) {
|
||||||
|
System.out.println("Adding index on \"timestamp\" field to collection " + this.messageCollection.getName());
|
||||||
|
this.messageCollection.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique));
|
||||||
|
}
|
||||||
|
if (!this.messageCollection.hasIndex("senderNickname")) {
|
||||||
|
System.out.println("Adding index on \"senderNickname\" field to collection " + this.messageCollection.getName());
|
||||||
|
this.messageCollection.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext));
|
||||||
|
}
|
||||||
|
if (!this.messageCollection.hasIndex("message")) {
|
||||||
|
System.out.println("Adding index on \"message\" field to collection " + this.messageCollection.getName());
|
||||||
|
this.messageCollection.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext));
|
||||||
|
}
|
||||||
|
var fields = List.of("timestamp", "senderNickname", "message");
|
||||||
|
for (var index : this.messageCollection.listIndices()) {
|
||||||
|
if (!fields.contains(index.getField())) {
|
||||||
|
System.out.println("Dropping unknown index " + index.getField() + " from collection " + index.getCollectionName());
|
||||||
|
this.messageCollection.dropIndex(index.getField());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addClient(ClientThread clientThread) {
|
public void addClient(ClientThread clientThread) {
|
||||||
|
@ -66,12 +92,12 @@ public class Channel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChannelUsersResponse.UserData> getUserData() {
|
public List<UserData> getUserData() {
|
||||||
List<ChannelUsersResponse.UserData> users = new ArrayList<>();
|
List<UserData> users = new ArrayList<>(this.connectedClients.size());
|
||||||
for (var clientThread : this.getConnectedClients()) {
|
for (var clientThread : this.getConnectedClients()) {
|
||||||
users.add(new ChannelUsersResponse.UserData(clientThread.getClientId(), clientThread.getClientNickname()));
|
users.add(clientThread.toData());
|
||||||
}
|
}
|
||||||
users.sort(Comparator.comparing(ChannelUsersResponse.UserData::getName));
|
users.sort(Comparator.comparing(UserData::getName));
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import lombok.Setter;
|
||||||
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.UserData;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
|
@ -122,6 +123,10 @@ public class ClientThread extends Thread {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserData toData() {
|
||||||
|
return new UserData(this.clientId, this.clientNickname);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.clientNickname + " (" + this.clientId + ")";
|
return this.clientNickname + " (" + this.clientId + ")";
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
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.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_core.msg.types.UserData;
|
||||||
|
import nl.andrewl.concord_server.cli.ServerCli;
|
||||||
import nl.andrewl.concord_server.config.ServerConfig;
|
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.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
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.nio.file.Path;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -52,33 +57,14 @@ 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()) {
|
for (var channelConfig : config.getChannels()) {
|
||||||
this.channelManager.addChannel(new Channel(
|
this.channelManager.addChannel(new Channel(
|
||||||
this,
|
this,
|
||||||
UUID.fromString(channelConfig.id()),
|
UUID.fromString(channelConfig.getId()),
|
||||||
channelConfig.name(),
|
channelConfig.getName(),
|
||||||
this.db.getCollection("channel-" + channelConfig.id())
|
this.db.getCollection("channel-" + channelConfig.getId())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
this.updateDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDatabase() {
|
|
||||||
for (var channel : this.channelManager.getChannels()) {
|
|
||||||
var col = channel.getMessageCollection();
|
|
||||||
if (!col.hasIndex("timestamp")) {
|
|
||||||
System.out.println("Adding timestamp index to collection for channel " + channel.getName());
|
|
||||||
col.createIndex("timestamp", IndexOptions.indexOptions(IndexType.NonUnique));
|
|
||||||
}
|
|
||||||
if (!col.hasIndex("senderNickname")) {
|
|
||||||
System.out.println("Adding senderNickname index to collection for channel " + channel.getName());
|
|
||||||
col.createIndex("senderNickname", IndexOptions.indexOptions(IndexType.Fulltext));
|
|
||||||
}
|
|
||||||
if (!col.hasIndex("message")) {
|
|
||||||
System.out.println("Adding message index to collection for channel " + channel.getName());
|
|
||||||
col.createIndex("message", IndexOptions.indexOptions(IndexType.Fulltext));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,17 +82,9 @@ public class ConcordServer implements Runnable {
|
||||||
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.
|
|
||||||
ServerMetaData metaData = new ServerMetaData(
|
|
||||||
this.config.name(),
|
|
||||||
this.channelManager.getChannels().stream()
|
|
||||||
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
|
|
||||||
.sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
);
|
|
||||||
// Immediately add the client to the default channel and send the initial welcome message.
|
// 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));
|
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), this.getMetaData()));
|
||||||
// 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);
|
||||||
|
@ -127,14 +105,52 @@ public class ConcordServer implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserData> getClients() {
|
||||||
|
return this.clients.values().stream()
|
||||||
|
.sorted(Comparator.comparing(ClientThread::getClientNickname))
|
||||||
|
.map(ClientThread::toData)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerMetaData getMetaData() {
|
||||||
|
return new ServerMetaData(
|
||||||
|
this.config.getName(),
|
||||||
|
this.channelManager.getChannels().stream()
|
||||||
|
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
|
||||||
|
.sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to every connected client.
|
||||||
|
* @param message The message to send.
|
||||||
|
*/
|
||||||
|
public void broadcast(Message message) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
|
||||||
|
try {
|
||||||
|
Serializer.writeMessage(message, baos);
|
||||||
|
byte[] data = baos.toByteArray();
|
||||||
|
for (var client : this.clients.values()) {
|
||||||
|
client.sendToClient(data);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
this.running = true;
|
this.running = true;
|
||||||
ServerSocket serverSocket;
|
ServerSocket serverSocket;
|
||||||
try {
|
try {
|
||||||
serverSocket = new ServerSocket(this.config.port());
|
serverSocket = new ServerSocket(this.config.getPort());
|
||||||
StringBuilder startupMessage = new StringBuilder();
|
StringBuilder startupMessage = new StringBuilder();
|
||||||
startupMessage.append("Opened server on port ").append(config.port()).append("\n");
|
startupMessage.append("Opened server on port ").append(config.getPort()).append("\n");
|
||||||
for (var channel : this.channelManager.getChannels()) {
|
for (var channel : this.channelManager.getChannels()) {
|
||||||
startupMessage.append("\tChannel \"").append(channel).append('\n');
|
startupMessage.append("\tChannel \"").append(channel).append('\n');
|
||||||
}
|
}
|
||||||
|
@ -151,6 +167,7 @@ public class ConcordServer implements Runnable {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
var server = new ConcordServer();
|
var server = new ConcordServer();
|
||||||
server.run();
|
new Thread(server).start();
|
||||||
|
new ServerCli(server).run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package nl.andrewl.concord_server.cli;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.cli.command.AddChannelCommand;
|
||||||
|
import nl.andrewl.concord_server.cli.command.ListClientsCommand;
|
||||||
|
import nl.andrewl.concord_server.cli.command.RemoveChannelCommand;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ServerCli implements Runnable {
|
||||||
|
private final ConcordServer server;
|
||||||
|
private final Map<String, ServerCliCommand> commands;
|
||||||
|
|
||||||
|
public ServerCli(ConcordServer server) {
|
||||||
|
this.server = server;
|
||||||
|
this.commands = new HashMap<>();
|
||||||
|
|
||||||
|
this.commands.put("list-clients", new ListClientsCommand());
|
||||||
|
this.commands.put("add-channel", new AddChannelCommand());
|
||||||
|
this.commands.put("remove-channel", new RemoveChannelCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
String line;
|
||||||
|
try {
|
||||||
|
while (this.server.isRunning() && (line = reader.readLine()) != null) {
|
||||||
|
if (!line.isBlank()) {
|
||||||
|
String[] words = line.split("\\s+");
|
||||||
|
String command = words[0];
|
||||||
|
String[] args = Arrays.copyOfRange(words, 1, words.length);
|
||||||
|
var cliCommand = this.commands.get(command.trim().toLowerCase());
|
||||||
|
if (cliCommand != null) {
|
||||||
|
try {
|
||||||
|
cliCommand.handle(this.server, args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.err.println("Unknown command.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.concord_server.cli;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
|
public interface ServerCliCommand {
|
||||||
|
void handle(ConcordServer server, String[] args) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package nl.andrewl.concord_server.cli.command;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_server.Channel;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
|
import nl.andrewl.concord_server.config.ServerConfig;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AddChannelCommand implements ServerCliCommand {
|
||||||
|
@Override
|
||||||
|
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||||
|
if (args.length < 1) {
|
||||||
|
System.err.println("Missing required name argument.");
|
||||||
|
}
|
||||||
|
String name = args[0].trim().toLowerCase().replaceAll("\\s+", "-");
|
||||||
|
if (name.isBlank()) {
|
||||||
|
System.err.println("Cannot create channel with blank name.");
|
||||||
|
}
|
||||||
|
if (server.getChannelManager().getChannelByName(name).isPresent()) {
|
||||||
|
System.err.println("Channel with that name already exists.");
|
||||||
|
}
|
||||||
|
String description = null;
|
||||||
|
if (args.length > 1) {
|
||||||
|
description = args[1].trim();
|
||||||
|
}
|
||||||
|
UUID id = server.getIdProvider().newId();
|
||||||
|
var channelConfig = new ServerConfig.ChannelConfig(id.toString(), name, description);
|
||||||
|
server.getConfig().getChannels().add(channelConfig);
|
||||||
|
server.getConfig().save();
|
||||||
|
|
||||||
|
var col = server.getDb().getCollection("channel-" + id);
|
||||||
|
server.getChannelManager().addChannel(new Channel(server, id, name, col));
|
||||||
|
server.broadcast(server.getMetaData());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package nl.andrewl.concord_server.cli.command;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
|
|
||||||
|
public class ListClientsCommand implements ServerCliCommand {
|
||||||
|
@Override
|
||||||
|
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||||
|
var users = server.getClients();
|
||||||
|
if (users.isEmpty()) {
|
||||||
|
System.out.println("There are no connected clients.");
|
||||||
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder("Online Users:\n");
|
||||||
|
for (var userData : users) {
|
||||||
|
sb.append("\t").append(userData.getName()).append(" (").append(userData.getId()).append(")\n");
|
||||||
|
}
|
||||||
|
System.out.print(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package nl.andrewl.concord_server.cli.command;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_server.Channel;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class RemoveChannelCommand implements ServerCliCommand {
|
||||||
|
@Override
|
||||||
|
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||||
|
if (args.length != 1) {
|
||||||
|
System.err.println("Missing required channel name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = args[0].trim().toLowerCase();
|
||||||
|
Optional<Channel> optionalChannel = server.getChannelManager().getChannelByName(name);
|
||||||
|
if (optionalChannel.isEmpty()) {
|
||||||
|
System.err.println("No channel with that name exists.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Channel channelToRemove = optionalChannel.get();
|
||||||
|
Channel alternative = null;
|
||||||
|
for (var c : server.getChannelManager().getChannels()) {
|
||||||
|
if (!c.equals(channelToRemove)) {
|
||||||
|
alternative = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alternative == null) {
|
||||||
|
System.err.println("No alternative channel could be found. A server must always have at least one channel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var client : channelToRemove.getConnectedClients()) {
|
||||||
|
server.getChannelManager().moveToChannel(client, alternative);
|
||||||
|
}
|
||||||
|
server.getChannelManager().removeChannel(channelToRemove);
|
||||||
|
server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName());
|
||||||
|
server.broadcast(server.getMetaData());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +1,43 @@
|
||||||
package nl.andrewl.concord_server.config;
|
package nl.andrewl.concord_server.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.java.Log;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_server.IdProvider;
|
import nl.andrewl.concord_server.IdProvider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public record ServerConfig(
|
@Data
|
||||||
String name,
|
@NoArgsConstructor
|
||||||
int port,
|
@AllArgsConstructor
|
||||||
|
public final class ServerConfig {
|
||||||
|
private String name;
|
||||||
|
private int port;
|
||||||
|
private int chatHistoryMaxCount;
|
||||||
|
private int chatHistoryDefaultCount;
|
||||||
|
private int maxMessageLength;
|
||||||
|
private List<ChannelConfig> channels;
|
||||||
|
|
||||||
// Global Channel configuration
|
/**
|
||||||
int chatHistoryMaxCount,
|
* The path at which this config is stored.
|
||||||
int chatHistoryDefaultCount,
|
*/
|
||||||
int maxMessageLength,
|
@JsonIgnore
|
||||||
|
private transient Path filePath;
|
||||||
|
|
||||||
ChannelConfig[] channels
|
@Data
|
||||||
) {
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public static record ChannelConfig (
|
public static final class ChannelConfig {
|
||||||
String id,
|
private String id;
|
||||||
String name,
|
private String name;
|
||||||
String description
|
private String description;
|
||||||
) {}
|
}
|
||||||
|
|
||||||
public static ServerConfig loadOrCreate(Path filePath, IdProvider idProvider) {
|
public static ServerConfig loadOrCreate(Path filePath, IdProvider idProvider) {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
@ -37,9 +49,8 @@ public record ServerConfig(
|
||||||
100,
|
100,
|
||||||
50,
|
50,
|
||||||
8192,
|
8192,
|
||||||
new ServerConfig.ChannelConfig[]{
|
List.of(new ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")),
|
||||||
new ServerConfig.ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")
|
filePath
|
||||||
}
|
|
||||||
);
|
);
|
||||||
try (var out = Files.newOutputStream(filePath)) {
|
try (var out = Files.newOutputStream(filePath)) {
|
||||||
mapper.writerWithDefaultPrettyPrinter().writeValue(out, config);
|
mapper.writerWithDefaultPrettyPrinter().writeValue(out, config);
|
||||||
|
@ -50,6 +61,7 @@ public record ServerConfig(
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class);
|
config = mapper.readValue(Files.newInputStream(filePath), ServerConfig.class);
|
||||||
|
config.setFilePath(filePath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
|
@ -57,4 +69,11 @@ public record ServerConfig(
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void save() throws IOException {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
try (var out = Files.newOutputStream(filePath)) {
|
||||||
|
mapper.writerWithDefaultPrettyPrinter().writeValue(out, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
||||||
if (optionalChannel.isPresent()) {
|
if (optionalChannel.isPresent()) {
|
||||||
var channel = optionalChannel.get();
|
var channel = optionalChannel.get();
|
||||||
var params = msg.getQueryAsMap();
|
var params = msg.getQueryAsMap();
|
||||||
Long count = this.getOrDefault(params, "count", (long) server.getConfig().chatHistoryDefaultCount());
|
Long count = this.getOrDefault(params, "count", (long) server.getConfig().getChatHistoryDefaultCount());
|
||||||
if (count > server.getConfig().chatHistoryMaxCount()) {
|
if (count > server.getConfig().getChatHistoryMaxCount()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Long from = this.getOrDefault(params, "from", null);
|
Long from = this.getOrDefault(params, "from", null);
|
||||||
|
|
Loading…
Reference in New Issue