Implemented private channels for N-person direct messaging.
This commit is contained in:
parent
2d8a0967dc
commit
17a372a5b7
|
@ -10,9 +10,9 @@ import com.googlecode.lanterna.terminal.Terminal;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewl.concord_client.event.EventManager;
|
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.ChatHistoryResponseHandler;
|
import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler;
|
||||||
import nl.andrewl.concord_client.event.handlers.ServerMetaDataHandler;
|
import nl.andrewl.concord_client.event.handlers.ServerMetaDataHandler;
|
||||||
|
import nl.andrewl.concord_client.event.handlers.ServerUsersHandler;
|
||||||
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;
|
||||||
|
@ -48,7 +48,7 @@ public class ConcordClient implements Runnable {
|
||||||
|
|
||||||
// Add event listeners.
|
// Add event listeners.
|
||||||
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
|
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
|
||||||
this.eventManager.addHandler(ChannelUsersResponse.class, new ChannelUsersResponseHandler());
|
this.eventManager.addHandler(ServerUsers.class, new ServerUsersHandler());
|
||||||
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());
|
this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler());
|
||||||
|
@ -70,10 +70,9 @@ public class ConcordClient implements Runnable {
|
||||||
this.serializer.writeMessage(new Identification(nickname), this.out);
|
this.serializer.writeMessage(new Identification(nickname), this.out);
|
||||||
Message reply = this.serializer.readMessage(this.in);
|
Message reply = this.serializer.readMessage(this.in);
|
||||||
if (reply instanceof ServerWelcome welcome) {
|
if (reply instanceof ServerWelcome welcome) {
|
||||||
var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getMetaData());
|
var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getCurrentChannelName(), welcome.getMetaData());
|
||||||
// Start fetching initial data for the channel we were initially put into.
|
// Start fetching initial data for the channel we were initially put into.
|
||||||
this.sendMessage(new ChannelUsersRequest(this.model.getCurrentChannelId()));
|
this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
|
||||||
this.sendMessage(new ChatHistoryRequest(this.model.getCurrentChannelId(), ""));
|
|
||||||
return model;
|
return model;
|
||||||
} 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.");
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface ClientModelListener {
|
public interface ClientModelListener {
|
||||||
default void channelMoved(UUID oldChannelId, UUID newChannelId) {}
|
default void channelMoved(UUID oldChannelId, String oldChannelName, UUID newChannelId, String newChannelName) {}
|
||||||
|
|
||||||
default void usersUpdated(List<UserData> users) {}
|
default void usersUpdated(List<UserData> users) {}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package nl.andrewl.concord_client.event.handlers;
|
||||||
|
|
||||||
import nl.andrewl.concord_client.ConcordClient;
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
import nl.andrewl.concord_client.event.MessageHandler;
|
import nl.andrewl.concord_client.event.MessageHandler;
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersRequest;
|
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
|
||||||
|
@ -15,8 +14,7 @@ import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
public class ChannelMovedHandler implements MessageHandler<MoveToChannel> {
|
public class ChannelMovedHandler implements MessageHandler<MoveToChannel> {
|
||||||
@Override
|
@Override
|
||||||
public void handle(MoveToChannel msg, ConcordClient client) throws Exception {
|
public void handle(MoveToChannel msg, ConcordClient client) throws Exception {
|
||||||
client.getModel().setCurrentChannelId(msg.getChannelId());
|
client.getModel().setCurrentChannel(msg.getId(), msg.getChannelName());
|
||||||
client.sendMessage(new ChatHistoryRequest(msg.getChannelId(), ""));
|
client.sendMessage(new ChatHistoryRequest(msg.getId(), ""));
|
||||||
client.sendMessage(new ChannelUsersRequest(msg.getChannelId()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
|
||||||
|
|
||||||
public class ChatHistoryResponseHandler implements MessageHandler<ChatHistoryResponse> {
|
public class ChatHistoryResponseHandler implements MessageHandler<ChatHistoryResponse> {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ChatHistoryResponse msg, ConcordClient client) throws Exception {
|
public void handle(ChatHistoryResponse msg, ConcordClient client) {
|
||||||
client.getModel().getChatHistory().setChats(msg.getMessages());
|
client.getModel().getChatHistory().setChats(msg.getMessages());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ServerUsers;
|
||||||
|
|
||||||
|
public class ServerUsersHandler implements MessageHandler<ServerUsers> {
|
||||||
|
@Override
|
||||||
|
public void handle(ServerUsers msg, ConcordClient client) {
|
||||||
|
client.getModel().setKnownUsers(msg.getUsers());
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,9 +53,7 @@ public class ChannelChatBox extends Panel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshBorder() {
|
public void refreshBorder() {
|
||||||
String name = client.getModel().getServerMetaData().getChannels().stream()
|
String name = client.getModel().getCurrentChannelName();
|
||||||
.filter(channelData -> channelData.getId().equals(client.getModel().getCurrentChannelId()))
|
|
||||||
.findAny().orElseThrow().getName();
|
|
||||||
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
|
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
|
||||||
this.chatBorder = Borders.doubleLine("#" + name);
|
this.chatBorder = Borders.doubleLine("#" + name);
|
||||||
this.chatBorder.setComponent(this.chatList);
|
this.chatBorder.setComponent(this.chatList);
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class ServerPanel extends Panel implements ClientModelListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelMoved(UUID oldChannelId, UUID newChannelId) {
|
public void channelMoved(UUID oldChannelId, String oldChannelName, UUID newChannelId, String newChannelName) {
|
||||||
this.getTextGUI().getGUIThread().invokeLater(() -> {
|
this.getTextGUI().getGUIThread().invokeLater(() -> {
|
||||||
this.channelList.setChannels();
|
this.channelList.setChannels();
|
||||||
this.channelChatBox.getChatList().clearItems();
|
this.channelChatBox.getChatList().clearItems();
|
||||||
|
|
|
@ -5,9 +5,10 @@ import com.googlecode.lanterna.gui2.Direction;
|
||||||
import com.googlecode.lanterna.gui2.LinearLayout;
|
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.MoveToChannel;
|
||||||
import nl.andrewl.concord_core.msg.types.UserData;
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class UserList extends Panel {
|
public class UserList extends Panel {
|
||||||
|
@ -22,7 +23,14 @@ public class UserList extends Panel {
|
||||||
this.removeAllComponents();
|
this.removeAllComponents();
|
||||||
for (var user : usersResponse) {
|
for (var user : usersResponse) {
|
||||||
Button b = new Button(user.getName(), () -> {
|
Button b = new Button(user.getName(), () -> {
|
||||||
System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
|
if (!client.getModel().getId().equals(user.getId())) {
|
||||||
|
System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
|
||||||
|
try {
|
||||||
|
client.sendMessage(new MoveToChannel(user.getId()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.addComponent(b);
|
this.addComponent(b);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,25 +17,29 @@ public class ClientModel {
|
||||||
private ServerMetaData serverMetaData;
|
private ServerMetaData serverMetaData;
|
||||||
|
|
||||||
private UUID currentChannelId;
|
private UUID currentChannelId;
|
||||||
|
private String currentChannelName;
|
||||||
private List<UserData> knownUsers;
|
private List<UserData> knownUsers;
|
||||||
private final ChatHistory chatHistory;
|
private final ChatHistory chatHistory;
|
||||||
|
|
||||||
private final List<ClientModelListener> modelListeners;
|
private final List<ClientModelListener> modelListeners;
|
||||||
|
|
||||||
public ClientModel(UUID id, String nickname, UUID currentChannelId, ServerMetaData serverMetaData) {
|
public ClientModel(UUID id, String nickname, UUID currentChannelId, String currentChannelName, ServerMetaData serverMetaData) {
|
||||||
this.modelListeners = new CopyOnWriteArrayList<>();
|
this.modelListeners = new CopyOnWriteArrayList<>();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
this.currentChannelId = currentChannelId;
|
this.currentChannelId = currentChannelId;
|
||||||
|
this.currentChannelName = currentChannelName;
|
||||||
this.serverMetaData = serverMetaData;
|
this.serverMetaData = serverMetaData;
|
||||||
this.knownUsers = new ArrayList<>();
|
this.knownUsers = new ArrayList<>();
|
||||||
this.chatHistory = new ChatHistory();
|
this.chatHistory = new ChatHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCurrentChannelId(UUID newChannelId) {
|
public void setCurrentChannel(UUID channelId, String channelName) {
|
||||||
UUID oldId = this.currentChannelId;
|
UUID oldId = this.currentChannelId;
|
||||||
this.currentChannelId = newChannelId;
|
String oldName = this.currentChannelName;
|
||||||
this.modelListeners.forEach(listener -> listener.channelMoved(oldId, newChannelId));
|
this.currentChannelId = channelId;
|
||||||
|
this.currentChannelName = channelName;
|
||||||
|
this.modelListeners.forEach(listener -> listener.channelMoved(oldId, oldName, channelId, channelName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setKnownUsers(List<UserData> users) {
|
public void setKnownUsers(List<UserData> users) {
|
||||||
|
|
|
@ -18,11 +18,11 @@ public class MessageUtils {
|
||||||
/**
|
/**
|
||||||
* Gets the number of bytes that the given string will occupy when it is
|
* Gets the number of bytes that the given string will occupy when it is
|
||||||
* serialized.
|
* serialized.
|
||||||
* @param s The string.
|
* @param s The string. This may be null.
|
||||||
* @return The number of bytes used to serialize the string.
|
* @return The number of bytes used to serialize the string.
|
||||||
*/
|
*/
|
||||||
public static int getByteSize(String s) {
|
public static int getByteSize(String s) {
|
||||||
return Integer.BYTES + s.getBytes(StandardCharsets.UTF_8).length;
|
return Integer.BYTES + (s == null ? 0 : s.getBytes(StandardCharsets.UTF_8).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,22 +61,35 @@ public class MessageUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeEnum(Enum<?> value, DataOutputStream o) throws IOException {
|
public static void writeEnum(Enum<?> value, DataOutputStream o) throws IOException {
|
||||||
o.writeInt(value.ordinal());
|
if (value == null) {
|
||||||
|
o.writeInt(-1);
|
||||||
|
} else {
|
||||||
|
o.writeInt(value.ordinal());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Enum<?>> T readEnum(Class<T> e, DataInputStream i) throws IOException {
|
public static <T extends Enum<?>> T readEnum(Class<T> e, DataInputStream i) throws IOException {
|
||||||
int ordinal = i.readInt();
|
int ordinal = i.readInt();
|
||||||
|
if (ordinal == -1) return null;
|
||||||
return e.getEnumConstants()[ordinal];
|
return e.getEnumConstants()[ordinal];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeUUID(UUID value, DataOutputStream o) throws IOException {
|
public static void writeUUID(UUID value, DataOutputStream o) throws IOException {
|
||||||
o.writeLong(value.getMostSignificantBits());
|
if (value == null) {
|
||||||
o.writeLong(value.getLeastSignificantBits());
|
o.writeLong(-1);
|
||||||
|
o.writeLong(-1);
|
||||||
|
} else {
|
||||||
|
o.writeLong(value.getMostSignificantBits());
|
||||||
|
o.writeLong(value.getLeastSignificantBits());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UUID readUUID(DataInputStream i) throws IOException {
|
public static UUID readUUID(DataInputStream i) throws IOException {
|
||||||
long a = i.readLong();
|
long a = i.readLong();
|
||||||
long b = i.readLong();
|
long b = i.readLong();
|
||||||
|
if (a == -1 && b == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return new UUID(a, b);
|
return new UUID(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,15 +108,19 @@ public class MessageUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Message> List<T> readList(Class<T> type, DataInputStream i) throws IOException, ReflectiveOperationException {
|
public static <T extends Message> List<T> readList(Class<T> type, DataInputStream i) throws IOException {
|
||||||
int size = i.readInt();
|
int size = i.readInt();
|
||||||
var constructor = type.getConstructor();
|
try {
|
||||||
List<T> items = new ArrayList<>(size);
|
var constructor = type.getConstructor();
|
||||||
for (int k = 0; k < size; k++) {
|
List<T> items = new ArrayList<>(size);
|
||||||
var item = constructor.newInstance();
|
for (int k = 0; k < size; k++) {
|
||||||
item.read(i);
|
var item = constructor.newInstance();
|
||||||
items.add(item);
|
item.read(i);
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,10 @@ public class Serializer {
|
||||||
registerType(4, ChatHistoryRequest.class);
|
registerType(4, ChatHistoryRequest.class);
|
||||||
registerType(5, ChatHistoryResponse.class);
|
registerType(5, ChatHistoryResponse.class);
|
||||||
registerType(6, ChannelUsersRequest.class);
|
registerType(6, ChannelUsersRequest.class);
|
||||||
registerType(7, ChannelUsersResponse.class);
|
registerType(7, ServerUsers.class);
|
||||||
registerType(8, ServerMetaData.class);
|
registerType(8, ServerMetaData.class);
|
||||||
registerType(9, Error.class);
|
registerType(9, Error.class);
|
||||||
|
registerType(10, CreateThread.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,6 +15,7 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Deprecated
|
||||||
public class ChannelUsersRequest implements Message {
|
public class ChannelUsersRequest implements Message {
|
||||||
private UUID channelId;
|
private UUID channelId;
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,12 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
* the users in the channel that a client is in has changed. For example, when
|
* the users in the channel that a client is in has changed. For example, when
|
||||||
* a user leaves a channel, all others in that channel will be sent this message
|
* a user leaves a channel, all others in that channel will be sent this message
|
||||||
* to indicate that update.
|
* to indicate that update.
|
||||||
|
* @deprecated Clients will be updated via a {@link ServerUsers} message.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Deprecated
|
||||||
public class ChannelUsersResponse implements Message {
|
public class ChannelUsersResponse implements Message {
|
||||||
private List<UserData> users;
|
private List<UserData> users;
|
||||||
|
|
||||||
|
@ -36,10 +38,6 @@ public class ChannelUsersResponse implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
try {
|
this.users = readList(UserData.class, i);
|
||||||
this.users = readList(UserData.class, i);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -7,6 +8,7 @@ import nl.andrewl.concord_core.msg.Message;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
@ -16,13 +18,18 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class Chat implements Message {
|
public class Chat implements Message {
|
||||||
|
private static final long ID_NONE = 0;
|
||||||
|
|
||||||
|
private UUID id;
|
||||||
private UUID senderId;
|
private UUID senderId;
|
||||||
private String senderNickname;
|
private String senderNickname;
|
||||||
private long timestamp;
|
private long timestamp;
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
|
public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
|
||||||
|
this.id = null;
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
this.senderNickname = senderNickname;
|
this.senderNickname = senderNickname;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
@ -40,6 +47,7 @@ public class Chat implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
|
writeUUID(this.id, o);
|
||||||
writeUUID(this.senderId, o);
|
writeUUID(this.senderId, o);
|
||||||
writeString(this.senderNickname, o);
|
writeString(this.senderNickname, o);
|
||||||
o.writeLong(this.timestamp);
|
o.writeLong(this.timestamp);
|
||||||
|
@ -48,6 +56,7 @@ public class Chat implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
|
this.id = readUUID(i);
|
||||||
this.senderId = readUUID(i);
|
this.senderId = readUUID(i);
|
||||||
this.senderNickname = readString(i);
|
this.senderNickname = readString(i);
|
||||||
this.timestamp = i.readLong();
|
this.timestamp = i.readLong();
|
||||||
|
@ -63,6 +72,7 @@ public class Chat implements Message {
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o.getClass().equals(this.getClass())) {
|
if (o.getClass().equals(this.getClass())) {
|
||||||
Chat other = (Chat) o;
|
Chat other = (Chat) o;
|
||||||
|
if (Objects.equals(this.getId(), other.getId())) return true;
|
||||||
return this.getSenderId().equals(other.getSenderId()) &&
|
return this.getSenderId().equals(other.getSenderId()) &&
|
||||||
this.getTimestamp() == other.getTimestamp() &&
|
this.getTimestamp() == other.getTimestamp() &&
|
||||||
this.getSenderNickname().equals(other.getSenderNickname()) &&
|
this.getSenderNickname().equals(other.getSenderNickname()) &&
|
||||||
|
|
|
@ -41,6 +41,9 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
* <li><code>to</code> - ISO-8601 timestamp indicating the timestamp
|
* <li><code>to</code> - ISO-8601 timestamp indicating the timestamp
|
||||||
* before which messages should be fetched. Only messages before this
|
* before which messages should be fetched. Only messages before this
|
||||||
* point in time are returned.</li>
|
* point in time are returned.</li>
|
||||||
|
* <li><code>id</code> - A single message id to fetch. If this parameter
|
||||||
|
* is present, all others are ignored, and a list containing the single
|
||||||
|
* message is returned, if it could be found, otherwise an empty list.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This message is sent by clients when they indicate that they would like to
|
||||||
|
* create a new thread in their current channel.
|
||||||
|
* <p>
|
||||||
|
* Conversely, this message is also sent by the server when a thread has
|
||||||
|
* been created by someone, and all clients need to be notified so that they
|
||||||
|
* can properly display to the user that a message has been turned into a
|
||||||
|
* thread.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CreateThread implements Message {
|
||||||
|
/**
|
||||||
|
* The id of the message from which the thread will be created. This will
|
||||||
|
* serve as the entry point of the thread, and the unique identifier for the
|
||||||
|
* thread.
|
||||||
|
*/
|
||||||
|
private UUID messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title for the thread. This may be null, in which case the thread does
|
||||||
|
* not have any title.
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getByteCount() {
|
||||||
|
return UUID_BYTES + getByteSize(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutputStream o) throws IOException {
|
||||||
|
writeUUID(this.messageId, o);
|
||||||
|
writeString(this.title, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(DataInputStream i) throws IOException {
|
||||||
|
this.messageId = readUUID(i);
|
||||||
|
this.title = readString(i);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
@ -19,28 +20,48 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
* Conversely, a client can send this request to the server to indicate that
|
* Conversely, a client can send this request to the server to indicate that
|
||||||
* they would like to switch to the specified channel.
|
* they would like to switch to the specified channel.
|
||||||
* </p>
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Clients can also send this message and provide the id of another client
|
||||||
|
* to request that they enter a private message channel with the referenced
|
||||||
|
* client.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class MoveToChannel implements Message {
|
public class MoveToChannel implements Message {
|
||||||
private UUID channelId;
|
/**
|
||||||
|
* The id of the channel that the client is requesting or being moved to, or
|
||||||
|
* the id of another client that the user wishes to begin private messaging
|
||||||
|
* with.
|
||||||
|
*/
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the channel that the client is moved to. This is null in
|
||||||
|
* cases where the client is requesting to move to a channel, and is only
|
||||||
|
* provided by the server when it moves a client.
|
||||||
|
*/
|
||||||
|
private String channelName;
|
||||||
|
|
||||||
public MoveToChannel(UUID channelId) {
|
public MoveToChannel(UUID channelId) {
|
||||||
this.channelId = channelId;
|
this.id = channelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return UUID_BYTES;
|
return UUID_BYTES + getByteSize(this.channelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
writeUUID(this.channelId, o);
|
writeUUID(this.id, o);
|
||||||
|
writeString(this.channelName, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.channelId = readUUID(i);
|
this.id = readUUID(i);
|
||||||
|
this.channelName = readString(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ import java.util.UUID;
|
||||||
|
|
||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata is sent by the server to clients to inform them of the structure of
|
||||||
|
* the server. This includes basic information about the server's own properties
|
||||||
|
* as well as information about all top-level channels.
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@ -34,13 +39,13 @@ public class ServerMetaData implements Message {
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.name = readString(i);
|
this.name = readString(i);
|
||||||
try {
|
this.channels = readList(ChannelData.class, i);
|
||||||
this.channels = readList(ChannelData.class, i);
|
|
||||||
} catch (ReflectiveOperationException e) {
|
|
||||||
throw new IOException("Reflection exception", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a top-level channel in the server which is visible and
|
||||||
|
* joinable for a user.
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
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 static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This message is sent from the server to the client whenever a change happens
|
||||||
|
* which requires the server to notify clients about a change of the list of
|
||||||
|
* global users.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ServerUsers 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 {
|
||||||
|
this.users = readList(UserData.class, i);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,17 +22,19 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
public class ServerWelcome implements Message {
|
public class ServerWelcome implements Message {
|
||||||
private UUID clientId;
|
private UUID clientId;
|
||||||
private UUID currentChannelId;
|
private UUID currentChannelId;
|
||||||
|
private String currentChannelName;
|
||||||
private ServerMetaData metaData;
|
private ServerMetaData metaData;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return 2 * UUID_BYTES + this.metaData.getByteCount();
|
return 2 * UUID_BYTES + getByteSize(this.currentChannelName) + this.metaData.getByteCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
writeUUID(this.clientId, o);
|
writeUUID(this.clientId, o);
|
||||||
writeUUID(this.currentChannelId, o);
|
writeUUID(this.currentChannelId, o);
|
||||||
|
writeString(this.currentChannelName, o);
|
||||||
this.metaData.write(o);
|
this.metaData.write(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ public class ServerWelcome implements Message {
|
||||||
this.clientId = readUUID(i);
|
this.clientId = readUUID(i);
|
||||||
this.currentChannelId = readUUID(i);
|
this.currentChannelId = readUUID(i);
|
||||||
this.metaData = new ServerMetaData();
|
this.metaData = new ServerMetaData();
|
||||||
|
this.currentChannelName = readString(i);
|
||||||
this.metaData.read(i);
|
this.metaData.read(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package nl.andrewl.concord_server;
|
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public class ChannelManager {
|
|
||||||
private final ConcordServer server;
|
|
||||||
private final Map<String, Channel> channelNameMap;
|
|
||||||
private final Map<UUID, Channel> channelIdMap;
|
|
||||||
|
|
||||||
public ChannelManager(ConcordServer server) {
|
|
||||||
this.server = server;
|
|
||||||
this.channelNameMap = new ConcurrentHashMap<>();
|
|
||||||
this.channelIdMap = new ConcurrentHashMap<>();
|
|
||||||
// Initialize the channels according to what's defined in the server's config.
|
|
||||||
for (var channelConfig : server.getConfig().getChannels()) {
|
|
||||||
this.addChannel(new Channel(
|
|
||||||
server,
|
|
||||||
UUID.fromString(channelConfig.getId()),
|
|
||||||
channelConfig.getName(),
|
|
||||||
server.getDb().getCollection("channel-" + channelConfig.getId())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Channel> getChannels() {
|
|
||||||
return Set.copyOf(this.channelIdMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Channel> getDefaultChannel() {
|
|
||||||
var optionalGeneral = this.getChannelByName("general");
|
|
||||||
if (optionalGeneral.isPresent()) {
|
|
||||||
return optionalGeneral;
|
|
||||||
}
|
|
||||||
for (var channel : this.getChannels()) {
|
|
||||||
return Optional.of(channel);
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Channel> getChannelByName(String name) {
|
|
||||||
return Optional.ofNullable(this.channelNameMap.get(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Channel> getChannelById(UUID id) {
|
|
||||||
return Optional.ofNullable(this.channelIdMap.get(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void moveToChannel(ClientThread client, Channel channel) {
|
|
||||||
if (client.getCurrentChannel() != null) {
|
|
||||||
var previousChannel = client.getCurrentChannel();
|
|
||||||
previousChannel.removeClient(client);
|
|
||||||
}
|
|
||||||
channel.addClient(client);
|
|
||||||
client.setCurrentChannel(channel);
|
|
||||||
client.sendToClient(new MoveToChannel(channel.getId()));
|
|
||||||
System.out.println("Moved client " + client + " to channel " + channel);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
package nl.andrewl.concord_server;
|
|
||||||
|
|
||||||
public class ChatThread {
|
|
||||||
}
|
|
|
@ -1,26 +1,27 @@
|
||||||
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.Serializer;
|
||||||
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_server.channel.ChannelManager;
|
||||||
import nl.andrewl.concord_core.msg.types.UserData;
|
|
||||||
import nl.andrewl.concord_server.cli.ServerCli;
|
import nl.andrewl.concord_server.cli.ServerCli;
|
||||||
|
import nl.andrewl.concord_server.client.ClientManager;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import nl.andrewl.concord_server.config.ServerConfig;
|
import nl.andrewl.concord_server.config.ServerConfig;
|
||||||
|
import nl.andrewl.concord_server.event.EventManager;
|
||||||
|
import nl.andrewl.concord_server.util.IdProvider;
|
||||||
|
import nl.andrewl.concord_server.util.UUIDProvider;
|
||||||
import org.dizitart.no2.Nitrite;
|
import org.dizitart.no2.Nitrite;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
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.concurrent.ExecutorService;
|
||||||
import java.util.Map;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.UUID;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,7 +31,6 @@ public class ConcordServer implements Runnable {
|
||||||
private static final Path CONFIG_FILE = Path.of("server-config.json");
|
private static final Path CONFIG_FILE = Path.of("server-config.json");
|
||||||
private static final Path DATABASE_FILE = Path.of("concord-server.db");
|
private static final Path DATABASE_FILE = Path.of("concord-server.db");
|
||||||
|
|
||||||
private final Map<UUID, ClientThread> clients;
|
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
|
|
||||||
|
@ -79,6 +79,12 @@ public class ConcordServer implements Runnable {
|
||||||
@Getter
|
@Getter
|
||||||
private final ChannelManager channelManager;
|
private final ChannelManager channelManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager that handles the collection of clients connected to this server.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private final ClientManager clientManager;
|
||||||
|
|
||||||
private final DiscoveryServerPublisher discoveryServerPublisher;
|
private final DiscoveryServerPublisher discoveryServerPublisher;
|
||||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
@ -87,51 +93,13 @@ public class ConcordServer implements Runnable {
|
||||||
this.config = ServerConfig.loadOrCreate(CONFIG_FILE, idProvider);
|
this.config = ServerConfig.loadOrCreate(CONFIG_FILE, idProvider);
|
||||||
this.discoveryServerPublisher = new DiscoveryServerPublisher(this.config);
|
this.discoveryServerPublisher = new DiscoveryServerPublisher(this.config);
|
||||||
this.db = Nitrite.builder().filePath(DATABASE_FILE.toFile()).openOrCreate();
|
this.db = Nitrite.builder().filePath(DATABASE_FILE.toFile()).openOrCreate();
|
||||||
this.clients = new ConcurrentHashMap<>(32);
|
|
||||||
this.eventManager = new EventManager(this);
|
this.eventManager = new EventManager(this);
|
||||||
this.channelManager = new ChannelManager(this);
|
this.channelManager = new ChannelManager(this);
|
||||||
|
this.clientManager = new ClientManager(this);
|
||||||
this.serverSocket = new ServerSocket(this.config.getPort());
|
this.serverSocket = new ServerSocket(this.config.getPort());
|
||||||
this.serializer = new Serializer();
|
this.serializer = new Serializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
public void registerClient(Identification identification, ClientThread clientThread) {
|
|
||||||
var id = this.idProvider.newId();
|
|
||||||
System.out.printf("Client \"%s\" joined with id %s.\n", identification.getNickname(), id);
|
|
||||||
this.clients.put(id, clientThread);
|
|
||||||
clientThread.setClientId(id);
|
|
||||||
clientThread.setClientNickname(identification.getNickname());
|
|
||||||
// Immediately add the client to the default channel and send the initial welcome message.
|
|
||||||
var defaultChannel = this.channelManager.getDefaultChannel().orElseThrow();
|
|
||||||
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.
|
|
||||||
defaultChannel.addClient(clientThread);
|
|
||||||
clientThread.setCurrentChannel(defaultChannel);
|
|
||||||
System.out.println("Moved client " + clientThread + " to " + defaultChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
var client = this.clients.remove(clientId);
|
|
||||||
if (client != null) {
|
|
||||||
client.getCurrentChannel().removeClient(client);
|
|
||||||
client.shutdown();
|
|
||||||
System.out.println("Client " + client + " has disconnected.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return True if the server is currently running, meaning it is accepting
|
* @return True if the server is currently running, meaning it is accepting
|
||||||
* connections, or false otherwise.
|
* connections, or false otherwise.
|
||||||
|
@ -155,13 +123,6 @@ public class ConcordServer implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserData> getClients() {
|
|
||||||
return this.clients.values().stream()
|
|
||||||
.sorted(Comparator.comparing(ClientThread::getClientNickname))
|
|
||||||
.map(ClientThread::toData)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerMetaData getMetaData() {
|
public ServerMetaData getMetaData() {
|
||||||
return new ServerMetaData(
|
return new ServerMetaData(
|
||||||
this.config.getName(),
|
this.config.getName(),
|
||||||
|
@ -172,24 +133,6 @@ public class ConcordServer implements Runnable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message to every connected client, ignoring any channels. All
|
|
||||||
* clients connected to this server will receive this message.
|
|
||||||
* @param message The message to send.
|
|
||||||
*/
|
|
||||||
public void broadcast(Message message) {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
|
|
||||||
try {
|
|
||||||
this.serializer.writeMessage(message, baos);
|
|
||||||
byte[] data = baos.toByteArray();
|
|
||||||
for (var client : this.clients.values()) {
|
|
||||||
client.sendToClient(data);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuts down the server cleanly by doing the following things:
|
* Shuts down the server cleanly by doing the following things:
|
||||||
* <ol>
|
* <ol>
|
||||||
|
@ -201,8 +144,8 @@ public class ConcordServer implements Runnable {
|
||||||
*/
|
*/
|
||||||
private void shutdown() {
|
private void shutdown() {
|
||||||
System.out.println("Shutting down the server.");
|
System.out.println("Shutting down the server.");
|
||||||
for (var clientId : this.clients.keySet()) {
|
for (var clientId : this.clientManager.getConnectedIds()) {
|
||||||
this.deregisterClient(clientId);
|
this.clientManager.deregisterClient(clientId);
|
||||||
}
|
}
|
||||||
this.scheduledExecutorService.shutdown();
|
this.scheduledExecutorService.shutdown();
|
||||||
this.executorService.shutdown();
|
this.executorService.shutdown();
|
||||||
|
@ -231,12 +174,16 @@ public class ConcordServer implements Runnable {
|
||||||
ClientThread clientThread = new ClientThread(socket, this);
|
ClientThread clientThread = new ClientThread(socket, this);
|
||||||
clientThread.start();
|
clientThread.start();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Could not accept new client connection: " + e.getMessage());
|
if (!e.getMessage().equalsIgnoreCase("socket closed")) {
|
||||||
|
System.err.println("Could not accept new client connection: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.shutdown();
|
this.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
var server = new ConcordServer();
|
var server = new ConcordServer();
|
||||||
new Thread(server).start();
|
new Thread(server).start();
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server.channel;
|
||||||
|
|
||||||
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.types.ChannelUsersResponse;
|
|
||||||
import nl.andrewl.concord_core.msg.types.UserData;
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
import org.dizitart.no2.IndexOptions;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.util.CollectionUtils;
|
||||||
import org.dizitart.no2.IndexType;
|
import org.dizitart.no2.IndexType;
|
||||||
import org.dizitart.no2.NitriteCollection;
|
import org.dizitart.no2.NitriteCollection;
|
||||||
|
|
||||||
|
@ -16,51 +16,36 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single communication area in which messages are sent by clients
|
* Represents a single communication area in which messages are sent by clients
|
||||||
* and received by all connected clients.
|
* and received by all connected clients. A channel is a top-level communication
|
||||||
|
* medium, and usually this is a server channel or private message between two
|
||||||
|
* clients in a server.
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class Channel {
|
public class Channel {
|
||||||
private final ConcordServer server;
|
private final ConcordServer server;
|
||||||
private UUID id;
|
private final UUID id;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private final Set<ClientThread> connectedClients;
|
private final Set<ClientThread> connectedClients;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A document collection which holds all messages created in this channel,
|
||||||
|
* indexed on id, timestamp, message, and sender's nickname.
|
||||||
|
*/
|
||||||
private final NitriteCollection messageCollection;
|
private final NitriteCollection messageCollection;
|
||||||
|
|
||||||
public Channel(ConcordServer server, UUID id, String name, NitriteCollection messageCollection) {
|
public Channel(ConcordServer server, UUID id, String name) {
|
||||||
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;
|
this.messageCollection = server.getDb().getCollection("channel-" + id);
|
||||||
this.initCollection();
|
CollectionUtils.ensureIndexes(this.messageCollection, Map.of(
|
||||||
}
|
"timestamp", IndexType.NonUnique,
|
||||||
|
"senderNickname", IndexType.Fulltext,
|
||||||
/**
|
"message", IndexType.Fulltext,
|
||||||
* Initializes this channel's nitrite database collection, which involves
|
"id", IndexType.Unique
|
||||||
* creating any indexes that don't yet exist.
|
));
|
||||||
*/
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,11 +55,11 @@ public class Channel {
|
||||||
*/
|
*/
|
||||||
public void addClient(ClientThread clientThread) {
|
public void addClient(ClientThread clientThread) {
|
||||||
this.connectedClients.add(clientThread);
|
this.connectedClients.add(clientThread);
|
||||||
try {
|
// try {
|
||||||
this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
// this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
||||||
} catch (IOException e) {
|
// } catch (IOException e) {
|
||||||
e.printStackTrace();
|
// e.printStackTrace();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,11 +69,11 @@ public class Channel {
|
||||||
*/
|
*/
|
||||||
public void removeClient(ClientThread clientThread) {
|
public void removeClient(ClientThread clientThread) {
|
||||||
this.connectedClients.remove(clientThread);
|
this.connectedClients.remove(clientThread);
|
||||||
try {
|
// try {
|
||||||
this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
// this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
||||||
} catch (IOException e) {
|
// } catch (IOException e) {
|
||||||
e.printStackTrace();
|
// e.printStackTrace();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,12 +110,13 @@ public class Channel {
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (!(o instanceof Channel channel)) return false;
|
if (!(o instanceof Channel channel)) return false;
|
||||||
return name.equals(channel.name);
|
if (Objects.equals(this.id, channel.getId())) return true;
|
||||||
|
return Objects.equals(this.name, channel.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(name);
|
return Objects.hash(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -0,0 +1,174 @@
|
||||||
|
package nl.andrewl.concord_server.channel;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.util.CollectionUtils;
|
||||||
|
import org.dizitart.no2.Document;
|
||||||
|
import org.dizitart.no2.IndexType;
|
||||||
|
import org.dizitart.no2.NitriteCollection;
|
||||||
|
import org.dizitart.no2.filters.Filters;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This manager is responsible for keeping track of all the channels in the
|
||||||
|
* server, and controlling modifications to them.
|
||||||
|
*/
|
||||||
|
public class ChannelManager {
|
||||||
|
private final ConcordServer server;
|
||||||
|
private final Map<String, Channel> channelNameMap;
|
||||||
|
private final Map<UUID, Channel> channelIdMap;
|
||||||
|
|
||||||
|
private final Map<Set<UUID>, Channel> privateChannels;
|
||||||
|
private final NitriteCollection privateChannelCollection;
|
||||||
|
|
||||||
|
public ChannelManager(ConcordServer server) {
|
||||||
|
this.server = server;
|
||||||
|
this.channelNameMap = new ConcurrentHashMap<>();
|
||||||
|
this.channelIdMap = new ConcurrentHashMap<>();
|
||||||
|
this.privateChannels = new ConcurrentHashMap<>();
|
||||||
|
this.privateChannelCollection = this.server.getDb().getCollection("private-channels");
|
||||||
|
CollectionUtils.ensureIndexes(this.privateChannelCollection, Map.of(
|
||||||
|
"idHash", IndexType.Unique,
|
||||||
|
"id", IndexType.Unique
|
||||||
|
));
|
||||||
|
// Initialize the channels according to what's defined in the server's config.
|
||||||
|
for (var channelConfig : server.getConfig().getChannels()) {
|
||||||
|
this.addChannel(new Channel(server, UUID.fromString(channelConfig.getId()), channelConfig.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Channel> getChannels() {
|
||||||
|
return Set.copyOf(this.channelIdMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Channel> getDefaultChannel() {
|
||||||
|
var optionalDefault = this.getChannelByName(this.server.getConfig().getDefaultChannel());
|
||||||
|
if (optionalDefault.isPresent()) {
|
||||||
|
return optionalDefault;
|
||||||
|
}
|
||||||
|
System.err.println("Could not find a channel with the name \"" + this.server.getConfig().getDefaultChannel() + "\".");
|
||||||
|
for (var channel : this.getChannels()) {
|
||||||
|
return Optional.of(channel);
|
||||||
|
}
|
||||||
|
System.err.println("Could not find any channel to use as a default channel.");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Channel> getChannelByName(String name) {
|
||||||
|
return Optional.ofNullable(this.channelNameMap.get(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Channel> getChannelById(UUID id) {
|
||||||
|
return Optional.ofNullable(this.channelIdMap.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a client to the given channel. This involves removing the client
|
||||||
|
* from whatever channel they're currently in, if any, moving them to the
|
||||||
|
* new channel, and sending them a message to indicate that it has been done.
|
||||||
|
* @param client The client to move.
|
||||||
|
* @param channel The channel to move the client to.
|
||||||
|
*/
|
||||||
|
public void moveToChannel(ClientThread client, Channel channel) {
|
||||||
|
if (client.getCurrentChannel() != null) {
|
||||||
|
var previousChannel = client.getCurrentChannel();
|
||||||
|
previousChannel.removeClient(client);
|
||||||
|
}
|
||||||
|
channel.addClient(client);
|
||||||
|
client.setCurrentChannel(channel);
|
||||||
|
client.sendToClient(new MoveToChannel(channel.getId(), channel.getName()));
|
||||||
|
System.out.println("Moved client " + client + " to channel " + channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates a private channel for the given client ids to be able to
|
||||||
|
* communicate together. No other clients are allowed to access the channel.
|
||||||
|
* @param clientIds The id of each client which should have access to the
|
||||||
|
* channel.
|
||||||
|
* @return The private channel.
|
||||||
|
*/
|
||||||
|
public Channel getPrivateChannel(Set<UUID> clientIds) {
|
||||||
|
if (clientIds.size() < 2) {
|
||||||
|
throw new IllegalArgumentException("At least 2 client ids are required for a private channel.");
|
||||||
|
}
|
||||||
|
return this.privateChannels.computeIfAbsent(clientIds, this::getPrivateChannelFromDatabase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a private channel, given the id of a client who is part of the
|
||||||
|
* channel, and the id of the channel.
|
||||||
|
* @param clientId The id of the client that's requesting the channel.
|
||||||
|
* @param channelId The id of the private channel.
|
||||||
|
* @return The private channel.
|
||||||
|
*/
|
||||||
|
public Optional<Channel> getPrivateChannel(UUID clientId, UUID channelId) {
|
||||||
|
Channel privateChannel = this.privateChannels.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getKey().contains(clientId) && entry.getValue().getId().equals(channelId))
|
||||||
|
.findAny().map(Map.Entry::getValue).orElse(null);
|
||||||
|
if (privateChannel == null) {
|
||||||
|
var cursor = this.privateChannelCollection.find(Filters.and(Filters.eq("id", channelId), Filters.in("clientIds", clientId)));
|
||||||
|
Document channelInfo = cursor.firstOrDefault();
|
||||||
|
if (channelInfo != null) {
|
||||||
|
privateChannel = new Channel(
|
||||||
|
this.server,
|
||||||
|
channelInfo.get("id", UUID.class),
|
||||||
|
channelInfo.get("name", String.class)
|
||||||
|
);
|
||||||
|
Set<UUID> clientIds = Set.of(channelInfo.get("clientIds", UUID[].class));
|
||||||
|
this.privateChannels.put(clientIds, privateChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(privateChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets and instantiates a private channel from information stored in the
|
||||||
|
* "private-channels" collection of the database, or creates it if it does
|
||||||
|
* not exist yet.
|
||||||
|
* @param clientIds The set of client ids that the channel is for.
|
||||||
|
* @return The private channel.
|
||||||
|
*/
|
||||||
|
private Channel getPrivateChannelFromDatabase(Set<UUID> clientIds) {
|
||||||
|
// First check if a private channel for these clients exists in the database.
|
||||||
|
String idHash = clientIds.stream().sorted().map(UUID::toString).collect(Collectors.joining());
|
||||||
|
var cursor = this.privateChannelCollection.find(Filters.eq("idHash", idHash));
|
||||||
|
Document channelInfo = cursor.firstOrDefault();
|
||||||
|
if (channelInfo != null) {
|
||||||
|
// If it does exist, instantiate a channel with its info.
|
||||||
|
return new Channel(
|
||||||
|
this.server,
|
||||||
|
channelInfo.get("id", UUID.class),
|
||||||
|
channelInfo.get("name", String.class)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Otherwise, create the channel anew and save it in the collection.
|
||||||
|
var channel = new Channel(this.server, this.server.getIdProvider().newId(), "Private Channel");
|
||||||
|
channelInfo = new Document(Map.of(
|
||||||
|
"idHash", idHash,
|
||||||
|
"id", channel.getId(),
|
||||||
|
"name", channel.getName(),
|
||||||
|
"clientIds", clientIds.toArray(new UUID[0])
|
||||||
|
));
|
||||||
|
this.privateChannelCollection.insert(channelInfo);
|
||||||
|
System.out.println("Created new private channel for clients: " + clientIds);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
package nl.andrewl.concord_server.cli.command;
|
package nl.andrewl.concord_server.cli.command;
|
||||||
|
|
||||||
import nl.andrewl.concord_server.Channel;
|
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.channel.Channel;
|
||||||
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
import nl.andrewl.concord_server.config.ServerConfig;
|
import nl.andrewl.concord_server.config.ServerConfig;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command adds a new channel to the server.
|
||||||
|
*/
|
||||||
public class AddChannelCommand implements ServerCliCommand {
|
public class AddChannelCommand implements ServerCliCommand {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ConcordServer server, String[] args) throws Exception {
|
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||||
|
@ -32,8 +35,7 @@ public class AddChannelCommand implements ServerCliCommand {
|
||||||
server.getConfig().getChannels().add(channelConfig);
|
server.getConfig().getChannels().add(channelConfig);
|
||||||
server.getConfig().save();
|
server.getConfig().save();
|
||||||
|
|
||||||
var col = server.getDb().getCollection("channel-" + id);
|
server.getChannelManager().addChannel(new Channel(server, id, name));
|
||||||
server.getChannelManager().addChannel(new Channel(server, id, name, col));
|
server.getClientManager().broadcast(server.getMetaData());
|
||||||
server.broadcast(server.getMetaData());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,13 @@ package nl.andrewl.concord_server.cli.command;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command shows a list of all clients that are currently connected to the server.
|
||||||
|
*/
|
||||||
public class ListClientsCommand implements ServerCliCommand {
|
public class ListClientsCommand implements ServerCliCommand {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ConcordServer server, String[] args) throws Exception {
|
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||||
var users = server.getClients();
|
var users = server.getClientManager().getClients();
|
||||||
if (users.isEmpty()) {
|
if (users.isEmpty()) {
|
||||||
System.out.println("There are no connected clients.");
|
System.out.println("There are no connected clients.");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package nl.andrewl.concord_server.cli.command;
|
package nl.andrewl.concord_server.cli.command;
|
||||||
|
|
||||||
import nl.andrewl.concord_server.Channel;
|
import nl.andrewl.concord_server.channel.Channel;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ public class RemoveChannelCommand implements ServerCliCommand {
|
||||||
server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName());
|
server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName());
|
||||||
server.getConfig().getChannels().removeIf(channelConfig -> channelConfig.getName().equals(channelToRemove.getName()));
|
server.getConfig().getChannels().removeIf(channelConfig -> channelConfig.getName().equals(channelToRemove.getName()));
|
||||||
server.getConfig().save();
|
server.getConfig().save();
|
||||||
server.broadcast(server.getMetaData());
|
server.getClientManager().broadcast(server.getMetaData());
|
||||||
System.out.println("Removed the channel " + channelToRemove);
|
System.out.println("Removed the channel " + channelToRemove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package nl.andrewl.concord_server.client;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_core.msg.types.Identification;
|
||||||
|
import nl.andrewl.concord_core.msg.types.ServerUsers;
|
||||||
|
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
||||||
|
import nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client manager is responsible for managing the set of clients connected
|
||||||
|
* to a server.
|
||||||
|
*/
|
||||||
|
public class ClientManager {
|
||||||
|
private final ConcordServer server;
|
||||||
|
private final Map<UUID, ClientThread> clients;
|
||||||
|
|
||||||
|
public ClientManager(ConcordServer server) {
|
||||||
|
this.server = server;
|
||||||
|
this.clients = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public void registerClient(Identification identification, ClientThread clientThread) {
|
||||||
|
var id = this.server.getIdProvider().newId();
|
||||||
|
System.out.printf("Client \"%s\" joined with id %s.\n", identification.getNickname(), id);
|
||||||
|
this.clients.put(id, clientThread);
|
||||||
|
clientThread.setClientId(id);
|
||||||
|
clientThread.setClientNickname(identification.getNickname());
|
||||||
|
// Immediately add the client to the default channel and send the initial welcome message.
|
||||||
|
var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
|
||||||
|
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
|
||||||
|
// It is important that we send the welcome message first. The client expects this as the initial response to their identification message.
|
||||||
|
defaultChannel.addClient(clientThread);
|
||||||
|
clientThread.setCurrentChannel(defaultChannel);
|
||||||
|
System.out.println("Moved client " + clientThread + " to " + defaultChannel);
|
||||||
|
this.broadcast(new ServerUsers(this.getClients()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
var client = this.clients.remove(clientId);
|
||||||
|
if (client != null) {
|
||||||
|
client.getCurrentChannel().removeClient(client);
|
||||||
|
client.shutdown();
|
||||||
|
System.out.println("Client " + client + " has disconnected.");
|
||||||
|
this.broadcast(new ServerUsers(this.getClients()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to every connected client, ignoring any channels. All
|
||||||
|
* clients connected to this server will receive this message.
|
||||||
|
* @param message The message to send.
|
||||||
|
*/
|
||||||
|
public void broadcast(Message message) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
|
||||||
|
try {
|
||||||
|
this.server.getSerializer().writeMessage(message, baos);
|
||||||
|
byte[] data = baos.toByteArray();
|
||||||
|
for (var client : this.clients.values()) {
|
||||||
|
client.sendToClient(data);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserData> getClients() {
|
||||||
|
return this.clients.values().stream()
|
||||||
|
.sorted(Comparator.comparing(ClientThread::getClientNickname))
|
||||||
|
.map(ClientThread::toData)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<UUID> getConnectedIds() {
|
||||||
|
return this.clients.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ClientThread> getClientById(UUID id) {
|
||||||
|
return Optional.ofNullable(this.clients.get(id));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server.client;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
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 nl.andrewl.concord_core.msg.types.UserData;
|
||||||
|
import nl.andrewl.concord_server.channel.Channel;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
|
@ -109,7 +111,7 @@ public class ClientThread extends Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.clientId != null) {
|
if (this.clientId != null) {
|
||||||
this.server.deregisterClient(this.clientId);
|
this.server.getClientManager().deregisterClient(this.clientId);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!this.socket.isClosed()) {
|
if (!this.socket.isClosed()) {
|
||||||
|
@ -133,7 +135,7 @@ public class ClientThread extends Thread {
|
||||||
try {
|
try {
|
||||||
var msg = this.server.getSerializer().readMessage(this.in);
|
var msg = this.server.getSerializer().readMessage(this.in);
|
||||||
if (msg instanceof Identification id) {
|
if (msg instanceof Identification id) {
|
||||||
this.server.registerClient(id, this);
|
this.server.getClientManager().registerClient(id, this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
|
@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_server.IdProvider;
|
import nl.andrewl.concord_server.util.IdProvider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
@ -23,6 +23,7 @@ public final class ServerConfig {
|
||||||
private int chatHistoryMaxCount;
|
private int chatHistoryMaxCount;
|
||||||
private int chatHistoryDefaultCount;
|
private int chatHistoryDefaultCount;
|
||||||
private int maxMessageLength;
|
private int maxMessageLength;
|
||||||
|
private String defaultChannel;
|
||||||
private List<ChannelConfig> channels;
|
private List<ChannelConfig> channels;
|
||||||
|
|
||||||
private List<String> discoveryServers;
|
private List<String> discoveryServers;
|
||||||
|
@ -53,6 +54,7 @@ public final class ServerConfig {
|
||||||
100,
|
100,
|
||||||
50,
|
50,
|
||||||
8192,
|
8192,
|
||||||
|
"general",
|
||||||
List.of(new ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")),
|
List.of(new ChannelConfig(idProvider.newId().toString(), "general", "Default channel for general discussion.")),
|
||||||
List.of(),
|
List.of(),
|
||||||
filePath
|
filePath
|
||||||
|
|
|
@ -1,16 +1,36 @@
|
||||||
package nl.andrewl.concord_server.event;
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.Error;
|
||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles client requests to move to another channel.
|
* Handles client requests to move to another channel. We first check if the id
|
||||||
|
* which the client sent refers to a channel, in which case we move them to that
|
||||||
|
* channel. Otherwise, we look for a client with that id, and try to move the
|
||||||
|
* requester into a private channel with them.
|
||||||
*/
|
*/
|
||||||
public class ChannelMoveHandler implements MessageHandler<MoveToChannel> {
|
public class ChannelMoveHandler implements MessageHandler<MoveToChannel> {
|
||||||
@Override
|
@Override
|
||||||
public void handle(MoveToChannel msg, ClientThread client, ConcordServer server) {
|
public void handle(MoveToChannel msg, ClientThread client, ConcordServer server) {
|
||||||
var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
|
var optionalChannel = server.getChannelManager().getChannelById(msg.getId());
|
||||||
optionalChannel.ifPresent(channel -> server.getChannelManager().moveToChannel(client, channel));
|
if (optionalChannel.isPresent()) {
|
||||||
|
server.getChannelManager().moveToChannel(client, optionalChannel.get());
|
||||||
|
} else {
|
||||||
|
var optionalClient = server.getClientManager().getClientById(msg.getId());
|
||||||
|
if (optionalClient.isPresent()) {
|
||||||
|
var privateChannel = server.getChannelManager().getPrivateChannel(Set.of(
|
||||||
|
client.getClientId(),
|
||||||
|
optionalClient.get().getClientId()
|
||||||
|
));
|
||||||
|
server.getChannelManager().moveToChannel(client, privateChannel);
|
||||||
|
} else {
|
||||||
|
client.sendToClient(Error.warning("Unknown channel or client id."));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersRequest;
|
import nl.andrewl.concord_core.msg.types.ChannelUsersRequest;
|
||||||
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
import nl.andrewl.concord_core.msg.types.ChannelUsersResponse;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
public class ChannelUsersRequestHandler implements MessageHandler<ChannelUsersRequest> {
|
public class ChannelUsersRequestHandler implements MessageHandler<ChannelUsersRequest> {
|
||||||
|
|
|
@ -2,13 +2,18 @@ package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
import nl.andrewl.concord_core.msg.types.Error;
|
import nl.andrewl.concord_core.msg.types.Error;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import org.dizitart.no2.Document;
|
import org.dizitart.no2.Document;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This handler is responsible for taking incoming chat messages and saving them
|
||||||
|
* to the channel's message collection, and then relaying the new message to all
|
||||||
|
* clients in the channel.
|
||||||
|
*/
|
||||||
public class ChatHandler implements MessageHandler<Chat> {
|
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 {
|
||||||
|
@ -16,16 +21,22 @@ public class ChatHandler implements MessageHandler<Chat> {
|
||||||
client.getCurrentChannel().sendMessage(Error.warning("Message is too long."));
|
client.getCurrentChannel().sendMessage(Error.warning("Message is too long."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
server.getExecutorService().submit(() -> {
|
/*
|
||||||
var collection = client.getCurrentChannel().getMessageCollection();
|
When we receive a message from the client, it will have a random UUID.
|
||||||
Document doc = new Document(Map.of(
|
A compromised client could try and send a duplicate or otherwise
|
||||||
"senderId", msg.getSenderId(),
|
malicious UUID, so we overwrite it with a server-generated id which we
|
||||||
"senderNickname", msg.getSenderNickname(),
|
know is safe.
|
||||||
"timestamp", msg.getTimestamp(),
|
*/
|
||||||
"message", msg.getMessage()
|
msg.setId(server.getIdProvider().newId());
|
||||||
));
|
var collection = client.getCurrentChannel().getMessageCollection();
|
||||||
collection.insert(doc);
|
Document doc = new Document(Map.of(
|
||||||
});
|
"id", msg.getId(),
|
||||||
|
"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());
|
System.out.printf("#%s | %s: %s\n", client.getCurrentChannel(), client.getClientNickname(), msg.getMessage());
|
||||||
client.getCurrentChannel().sendMessage(msg);
|
client.getCurrentChannel().sendMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ package nl.andrewl.concord_server.event;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
|
import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
|
||||||
import nl.andrewl.concord_server.Channel;
|
import nl.andrewl.concord_core.msg.types.Error;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.channel.Channel;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import org.dizitart.no2.*;
|
import org.dizitart.no2.*;
|
||||||
import org.dizitart.no2.filters.Filters;
|
import org.dizitart.no2.filters.Filters;
|
||||||
|
|
||||||
|
@ -17,10 +18,21 @@ import java.util.*;
|
||||||
public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequest> {
|
public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequest> {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
|
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
|
||||||
var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
|
// First try and find a public channel with the given id.
|
||||||
if (optionalChannel.isPresent()) {
|
var channel = server.getChannelManager().getChannelById(msg.getChannelId()).orElse(null);
|
||||||
var channel = optionalChannel.get();
|
if (channel == null) {
|
||||||
var params = msg.getQueryAsMap();
|
// Couldn't find a public channel, so look for a private channel this client is involved in.
|
||||||
|
channel = server.getChannelManager().getPrivateChannel(client.getClientId(), msg.getChannelId()).orElse(null);
|
||||||
|
}
|
||||||
|
// If we couldn't find a public or private channel, give up.
|
||||||
|
if (channel == null) {
|
||||||
|
client.sendToClient(Error.warning("Unknown channel id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var params = msg.getQueryAsMap();
|
||||||
|
if (params.containsKey("id")) {
|
||||||
|
this.handleIdRequest(client, channel, params.get("id"));
|
||||||
|
} else {
|
||||||
Long count = this.getOrDefault(params, "count", (long) server.getConfig().getChatHistoryDefaultCount());
|
Long count = this.getOrDefault(params, "count", (long) server.getConfig().getChatHistoryDefaultCount());
|
||||||
if (count > server.getConfig().getChatHistoryMaxCount()) {
|
if (count > server.getConfig().getChatHistoryMaxCount()) {
|
||||||
return;
|
return;
|
||||||
|
@ -31,14 +43,19 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getOrDefault(Map<String, String> params, String key, Long defaultValue) {
|
/**
|
||||||
String value = params.get(key);
|
* Handles a request for a single message from a channel.
|
||||||
if (value == null) return defaultValue;
|
* @param client The client who's requesting the data.
|
||||||
try {
|
* @param channel The channel in which to search for the message.
|
||||||
return Long.parseLong(value);
|
* @param id The id of the message.
|
||||||
} catch (NumberFormatException e) {
|
*/
|
||||||
return defaultValue;
|
private void handleIdRequest(ClientThread client, Channel channel, String id) {
|
||||||
|
var cursor = channel.getMessageCollection().find(Filters.eq("id", id));
|
||||||
|
List<Chat> chats = new ArrayList<>(1);
|
||||||
|
for (var doc : cursor) {
|
||||||
|
chats.add(this.read(doc));
|
||||||
}
|
}
|
||||||
|
client.sendToClient(new ChatHistoryResponse(channel.getId(), chats));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatHistoryResponse getResponse(Channel channel, long count, Long from, Long to) {
|
private ChatHistoryResponse getResponse(Channel channel, long count, Long from, Long to) {
|
||||||
|
@ -60,15 +77,43 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
||||||
|
|
||||||
List<Chat> chats = new ArrayList<>((int) count);
|
List<Chat> chats = new ArrayList<>((int) count);
|
||||||
for (Document doc : cursor) {
|
for (Document doc : cursor) {
|
||||||
chats.add(new Chat(
|
chats.add(this.read(doc));
|
||||||
doc.get("senderId", UUID.class),
|
|
||||||
doc.get("senderNickname", String.class),
|
|
||||||
doc.get("timestamp", Long.class),
|
|
||||||
doc.get("message", String.class)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
col.close();
|
col.close();
|
||||||
chats.sort(Comparator.comparingLong(Chat::getTimestamp));
|
chats.sort(Comparator.comparingLong(Chat::getTimestamp));
|
||||||
return new ChatHistoryResponse(channel.getId(), chats);
|
return new ChatHistoryResponse(channel.getId(), chats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to read a {@link Chat} from a document retrieved from a
|
||||||
|
* collection.
|
||||||
|
* @param doc The document to read.
|
||||||
|
* @return The chat that was read.
|
||||||
|
*/
|
||||||
|
private Chat read(Document doc) {
|
||||||
|
return new Chat(
|
||||||
|
doc.get("id", UUID.class),
|
||||||
|
doc.get("senderId", UUID.class),
|
||||||
|
doc.get("senderNickname", String.class),
|
||||||
|
doc.get("timestamp", Long.class),
|
||||||
|
doc.get("message", String.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get a long value or fall back to a default.
|
||||||
|
* @param params The parameters to check.
|
||||||
|
* @param key The key to get the value for.
|
||||||
|
* @param defaultValue The default value to return if no value exists.
|
||||||
|
* @return The value that was found, or the default value.
|
||||||
|
*/
|
||||||
|
private Long getOrDefault(Map<String, String> params, String key, Long defaultValue) {
|
||||||
|
String value = params.get(key);
|
||||||
|
if (value == null) return defaultValue;
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package nl.andrewl.concord_server.event;
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
public interface EventListener {
|
public interface EventListener {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
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.ChatHistoryRequest;
|
||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
import nl.andrewl.concord_server.event.*;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -25,7 +25,6 @@ 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());
|
this.messageHandlers.put(ChatHistoryRequest.class, new ChatHistoryRequestHandler());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package nl.andrewl.concord_server.event;
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_server.ClientThread;
|
import nl.andrewl.concord_server.client.ClientThread;
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
public interface MessageHandler<T extends Message> {
|
public interface MessageHandler<T extends Message> {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package nl.andrewl.concord_server.util;
|
||||||
|
|
||||||
|
import org.dizitart.no2.IndexOptions;
|
||||||
|
import org.dizitart.no2.IndexType;
|
||||||
|
import org.dizitart.no2.NitriteCollection;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CollectionUtils {
|
||||||
|
/**
|
||||||
|
* Ensures that the given nitrite collection has exactly the given set of
|
||||||
|
* indexes. It will remove any non-conforming indexes, and create new ones
|
||||||
|
* as necessary.
|
||||||
|
* @param collection The collection to operate on.
|
||||||
|
* @param indexMap A mapping containing keys referring to fields, and values
|
||||||
|
* that represent the type of index that should be on that
|
||||||
|
* field.
|
||||||
|
*/
|
||||||
|
public static void ensureIndexes(NitriteCollection collection, Map<String, IndexType> indexMap) {
|
||||||
|
for (var index : collection.listIndices()) {
|
||||||
|
var entry = indexMap.get(index.getField());
|
||||||
|
if (entry == null || !index.getIndexType().equals(entry)) {
|
||||||
|
collection.dropIndex(index.getField());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var entry : indexMap.entrySet()) {
|
||||||
|
if (!collection.hasIndex(entry.getKey())) {
|
||||||
|
collection.createIndex(entry.getKey(), IndexOptions.indexOptions(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server.util;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server.util;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
Loading…
Reference in New Issue