Merge pull request #5 from andrewlalis/messageRefactor
Refactored core message structure to use records.
This commit is contained in:
		
						commit
						c35fbbec9e
					
				| 
						 | 
					@ -20,6 +20,12 @@ import nl.andrewl.concord_core.msg.Encryption;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Serializer;
 | 
					import nl.andrewl.concord_core.msg.Serializer;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.*;
 | 
					import nl.andrewl.concord_core.msg.types.*;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.io.InputStream;
 | 
					import java.io.InputStream;
 | 
				
			||||||
| 
						 | 
					@ -84,8 +90,8 @@ public class ConcordClient implements Runnable {
 | 
				
			||||||
		this.serializer.writeMessage(new Identification(nickname, token), this.out);
 | 
							this.serializer.writeMessage(new Identification(nickname, token), 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.getCurrentChannelName(), welcome.getMetaData());
 | 
								var model = new ClientModel(welcome.clientId(), nickname, welcome.currentChannelId(), welcome.currentChannelName(), welcome.metaData());
 | 
				
			||||||
			this.saveSessionToken(welcome.getSessionToken(), tokensFile);
 | 
								this.saveSessionToken(welcome.sessionToken(), tokensFile);
 | 
				
			||||||
			// 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 ChatHistoryRequest(model.getCurrentChannelId(), ""));
 | 
								this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
 | 
				
			||||||
			return model;
 | 
								return model;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
package nl.andrewl.concord_client.event;
 | 
					package nl.andrewl.concord_client.event;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.concord_client.model.ChatHistory;
 | 
					import nl.andrewl.concord_client.model.ChatHistory;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface ChatHistoryListener {
 | 
					public interface ChatHistoryListener {
 | 
				
			||||||
	default void chatAdded(Chat chat) {}
 | 
						default void chatAdded(Chat chat) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,8 @@ 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.ChatHistoryRequest;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * When the client receives a {@link MoveToChannel} message, it means that the
 | 
					 * When the client receives a {@link MoveToChannel} message, it means that the
 | 
				
			||||||
| 
						 | 
					@ -16,7 +14,7 @@ import java.util.Map;
 | 
				
			||||||
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().setCurrentChannel(msg.getId(), msg.getChannelName());
 | 
							client.getModel().setCurrentChannel(msg.id(), msg.channelName());
 | 
				
			||||||
		client.sendMessage(new ChatHistoryRequest(msg.getId()));
 | 
							client.sendMessage(new ChatHistoryRequest(msg.id()));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,14 @@ 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.ChatHistoryResponse;
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ChatHistoryResponseHandler implements MessageHandler<ChatHistoryResponse> {
 | 
					public class ChatHistoryResponseHandler implements MessageHandler<ChatHistoryResponse> {
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public void handle(ChatHistoryResponse msg, ConcordClient client) {
 | 
						public void handle(ChatHistoryResponse msg, ConcordClient client) {
 | 
				
			||||||
		client.getModel().getChatHistory().setChats(msg.getMessages());
 | 
							client.getModel().getChatHistory().setChats(Arrays.asList(msg.messages()));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,9 +4,11 @@ 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.ServerUsers;
 | 
					import nl.andrewl.concord_core.msg.types.ServerUsers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ServerUsersHandler implements MessageHandler<ServerUsers> {
 | 
					public class ServerUsersHandler implements MessageHandler<ServerUsers> {
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public void handle(ServerUsers msg, ConcordClient client) {
 | 
						public void handle(ServerUsers msg, ConcordClient client) {
 | 
				
			||||||
		client.getModel().setKnownUsers(msg.getUsers());
 | 
							client.getModel().setKnownUsers(Arrays.asList(msg.users()));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ 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.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,15 +22,15 @@ public class ChannelList extends Panel {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void setChannels() {
 | 
						public void setChannels() {
 | 
				
			||||||
		this.removeAllComponents();
 | 
							this.removeAllComponents();
 | 
				
			||||||
		for (var channel : this.client.getModel().getServerMetaData().getChannels()) {
 | 
							for (var channel : this.client.getModel().getServerMetaData().channels()) {
 | 
				
			||||||
			String name = channel.getName();
 | 
								String name = channel.name();
 | 
				
			||||||
			if (client.getModel().getCurrentChannelId().equals(channel.getId())) {
 | 
								if (client.getModel().getCurrentChannelId().equals(channel.id())) {
 | 
				
			||||||
				name = "*" + name;
 | 
									name = "*" + name;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			Button b = new Button(name, () -> {
 | 
								Button b = new Button(name, () -> {
 | 
				
			||||||
				if (!client.getModel().getCurrentChannelId().equals(channel.getId())) {
 | 
									if (!client.getModel().getCurrentChannelId().equals(channel.id())) {
 | 
				
			||||||
					try {
 | 
										try {
 | 
				
			||||||
						client.sendMessage(new MoveToChannel(channel.getId()));
 | 
											client.sendMessage(new MoveToChannel(channel.id()));
 | 
				
			||||||
					} catch (IOException e) {
 | 
										} catch (IOException e) {
 | 
				
			||||||
						e.printStackTrace();
 | 
											e.printStackTrace();
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ package nl.andrewl.concord_client.gui;
 | 
				
			||||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
import nl.andrewl.concord_client.event.ChatHistoryListener;
 | 
					import nl.andrewl.concord_client.event.ChatHistoryListener;
 | 
				
			||||||
import nl.andrewl.concord_client.model.ChatHistory;
 | 
					import nl.andrewl.concord_client.model.ChatHistory;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This chat list shows a section of chat messages that have been sent in a
 | 
					 * This chat list shows a section of chat messages that have been sent in a
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import com.googlecode.lanterna.TerminalTextUtils;
 | 
				
			||||||
import com.googlecode.lanterna.graphics.ThemeDefinition;
 | 
					import com.googlecode.lanterna.graphics.ThemeDefinition;
 | 
				
			||||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
import com.googlecode.lanterna.gui2.TextGUIGraphics;
 | 
					import com.googlecode.lanterna.gui2.TextGUIGraphics;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.time.ZoneId;
 | 
					import java.time.ZoneId;
 | 
				
			||||||
| 
						 | 
					@ -20,10 +20,10 @@ public class ChatRenderer extends AbstractListBox.ListItemRenderer<Chat, ChatLis
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			graphics.applyThemeStyle(themeDefinition.getNormal());
 | 
								graphics.applyThemeStyle(themeDefinition.getNormal());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		graphics.putString(0, 0, chat.getSenderNickname());
 | 
							graphics.putString(0, 0, chat.senderNickname());
 | 
				
			||||||
		Instant timestamp = Instant.ofEpochMilli(chat.getTimestamp());
 | 
							Instant timestamp = Instant.ofEpochMilli(chat.timestamp());
 | 
				
			||||||
		String timeStr = timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm"));
 | 
							String timeStr = timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm"));
 | 
				
			||||||
		String label = chat.getSenderNickname() + "@" + timeStr + " : " + chat.getMessage();
 | 
							String label = chat.senderNickname() + "@" + timeStr + " : " + chat.message();
 | 
				
			||||||
		label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
 | 
							label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
 | 
				
			||||||
		while(TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) {
 | 
							while(TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) {
 | 
				
			||||||
			label += " ";
 | 
								label += " ";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ 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.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.UserData;
 | 
					import nl.andrewl.concord_core.msg.types.UserData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
| 
						 | 
					@ -22,11 +22,11 @@ public class UserList extends Panel {
 | 
				
			||||||
	public void updateUsers(List<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.name(), () -> {
 | 
				
			||||||
				if (!client.getModel().getId().equals(user.getId())) {
 | 
									if (!client.getModel().getId().equals(user.id())) {
 | 
				
			||||||
					System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
 | 
										System.out.println("Opening DM channel with user " + user.name() + ", id: " + user.id());
 | 
				
			||||||
					try {
 | 
										try {
 | 
				
			||||||
						client.sendMessage(new MoveToChannel(user.getId()));
 | 
											client.sendMessage(new MoveToChannel(user.id()));
 | 
				
			||||||
					} catch (IOException e) {
 | 
										} catch (IOException e) {
 | 
				
			||||||
						e.printStackTrace();
 | 
											e.printStackTrace();
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ package nl.andrewl.concord_client.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
import nl.andrewl.concord_client.event.ChatHistoryListener;
 | 
					import nl.andrewl.concord_client.event.ChatHistoryListener;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.concurrent.CopyOnWriteArrayList;
 | 
					import java.util.concurrent.CopyOnWriteArrayList;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,5 +3,9 @@ module concord_core {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	exports nl.andrewl.concord_core.util to concord_server, concord_client;
 | 
						exports nl.andrewl.concord_core.util to concord_server, concord_client;
 | 
				
			||||||
	exports nl.andrewl.concord_core.msg to concord_server, concord_client;
 | 
						exports nl.andrewl.concord_core.msg to concord_server, concord_client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	exports nl.andrewl.concord_core.msg.types to concord_server, concord_client;
 | 
						exports nl.andrewl.concord_core.msg.types to concord_server, concord_client;
 | 
				
			||||||
 | 
						exports nl.andrewl.concord_core.msg.types.client_setup to concord_client, concord_server;
 | 
				
			||||||
 | 
						exports nl.andrewl.concord_core.msg.types.chat to concord_client, concord_server;
 | 
				
			||||||
 | 
						exports nl.andrewl.concord_core.msg.types.channel to concord_client, concord_server;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg;
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.KeyData;
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.KeyData;
 | 
				
			||||||
import nl.andrewl.concord_core.util.Pair;
 | 
					import nl.andrewl.concord_core.util.Pair;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.crypto.Cipher;
 | 
					import javax.crypto.Cipher;
 | 
				
			||||||
| 
						 | 
					@ -64,20 +64,20 @@ public class Encryption {
 | 
				
			||||||
		// Receive and decode client's unencrypted key data.
 | 
							// Receive and decode client's unencrypted key data.
 | 
				
			||||||
		KeyData clientKeyData = (KeyData) serializer.readMessage(in);
 | 
							KeyData clientKeyData = (KeyData) serializer.readMessage(in);
 | 
				
			||||||
		PublicKey clientPublicKey = KeyFactory.getInstance("EC")
 | 
							PublicKey clientPublicKey = KeyFactory.getInstance("EC")
 | 
				
			||||||
				.generatePublic(new X509EncodedKeySpec(clientKeyData.getPublicKey()));
 | 
									.generatePublic(new X509EncodedKeySpec(clientKeyData.publicKey()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Compute secret key from client's public key and our private key.
 | 
							// Compute secret key from client's public key and our private key.
 | 
				
			||||||
		KeyAgreement ka = KeyAgreement.getInstance("ECDH");
 | 
							KeyAgreement ka = KeyAgreement.getInstance("ECDH");
 | 
				
			||||||
		ka.init(keyPair.getPrivate());
 | 
							ka.init(keyPair.getPrivate());
 | 
				
			||||||
		ka.doPhase(clientPublicKey, true);
 | 
							ka.doPhase(clientPublicKey, true);
 | 
				
			||||||
		byte[] secretKey = computeSecretKey(ka.generateSecret(), publicKey, clientKeyData.getPublicKey());
 | 
							byte[] secretKey = computeSecretKey(ka.generateSecret(), publicKey, clientKeyData.publicKey());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Initialize cipher streams.
 | 
							// Initialize cipher streams.
 | 
				
			||||||
		Cipher writeCipher = Cipher.getInstance("AES/CFB8/NoPadding");
 | 
							Cipher writeCipher = Cipher.getInstance("AES/CFB8/NoPadding");
 | 
				
			||||||
		Cipher readCipher = Cipher.getInstance("AES/CFB8/NoPadding");
 | 
							Cipher readCipher = Cipher.getInstance("AES/CFB8/NoPadding");
 | 
				
			||||||
		Key cipherKey = new SecretKeySpec(secretKey, "AES");
 | 
							Key cipherKey = new SecretKeySpec(secretKey, "AES");
 | 
				
			||||||
		writeCipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(iv));
 | 
							writeCipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(iv));
 | 
				
			||||||
		readCipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(clientKeyData.getIv()));
 | 
							readCipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(clientKeyData.iv()));
 | 
				
			||||||
		return new Pair<>(
 | 
							return new Pair<>(
 | 
				
			||||||
				new CipherInputStream(in, readCipher),
 | 
									new CipherInputStream(in, readCipher),
 | 
				
			||||||
				new CipherOutputStream(out, writeCipher)
 | 
									new CipherOutputStream(out, writeCipher)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,5 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg;
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.nio.charset.StandardCharsets;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Represents any message which can be sent over the network.
 | 
					 * Represents any message which can be sent over the network.
 | 
				
			||||||
 * <p>
 | 
					 * <p>
 | 
				
			||||||
| 
						 | 
					@ -14,26 +8,12 @@ import java.util.UUID;
 | 
				
			||||||
 * </p>
 | 
					 * </p>
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public interface Message {
 | 
					public interface Message {
 | 
				
			||||||
	/**
 | 
						@SuppressWarnings("unchecked")
 | 
				
			||||||
	 * @return The exact number of bytes that this message will use when written
 | 
						default <T extends Message> MessageType<T> getType() {
 | 
				
			||||||
	 * to a stream.
 | 
							return MessageType.get((Class<T>) this.getClass());
 | 
				
			||||||
	 */
 | 
						}
 | 
				
			||||||
	int getByteCount();
 | 
					
 | 
				
			||||||
 | 
						default int byteSize() {
 | 
				
			||||||
	/**
 | 
							return getType().byteSizeFunction().apply(this);
 | 
				
			||||||
	 * Writes this message to the given output stream.
 | 
						}
 | 
				
			||||||
	 * @param o The output stream to write to.
 | 
					 | 
				
			||||||
	 * @throws IOException If an error occurs while writing.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	void write(DataOutputStream o) throws IOException;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Reads all of this message's properties from the given input stream.
 | 
					 | 
				
			||||||
	 * <p>
 | 
					 | 
				
			||||||
	 *     The single byte type identifier has already been read.
 | 
					 | 
				
			||||||
	 * </p>
 | 
					 | 
				
			||||||
	 * @param i The input stream to read from.
 | 
					 | 
				
			||||||
	 * @throws IOException If an error occurs while reading.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	void read(DataInputStream i) throws IOException;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.util.ExtendedDataInputStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@FunctionalInterface
 | 
				
			||||||
 | 
					public interface MessageReader<T extends Message>{
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Reads all of this message's properties from the given input stream.
 | 
				
			||||||
 | 
						 * <p>
 | 
				
			||||||
 | 
						 *     The single byte type identifier has already been read.
 | 
				
			||||||
 | 
						 * </p>
 | 
				
			||||||
 | 
						 * @param in The input stream to read from.
 | 
				
			||||||
 | 
						 * @return The message that was read.
 | 
				
			||||||
 | 
						 * @throws IOException If an error occurs while reading.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						T read(ExtendedDataInputStream in) throws IOException;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,130 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.lang.reflect.Constructor;
 | 
				
			||||||
 | 
					import java.lang.reflect.RecordComponent;
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Record containing the components needed to read and write a given message.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     Also contains methods for automatically generating message type
 | 
				
			||||||
 | 
					 *     implementations for standard record-based messages.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 * @param <T> The type of message.
 | 
				
			||||||
 | 
					 * @param messageClass The class of the message.
 | 
				
			||||||
 | 
					 * @param byteSizeFunction A function that computes the byte size of the message.
 | 
				
			||||||
 | 
					 * @param reader A reader that can read messages from an input stream.
 | 
				
			||||||
 | 
					 * @param writer A writer that write messages from an input stream.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record MessageType<T extends Message>(
 | 
				
			||||||
 | 
							Class<T> messageClass,
 | 
				
			||||||
 | 
							Function<T, Integer> byteSizeFunction,
 | 
				
			||||||
 | 
							MessageReader<T> reader,
 | 
				
			||||||
 | 
							MessageWriter<T> writer
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						private static final Map<Class<?>, MessageType<?>> generatedMessageTypes = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Gets the {@link MessageType} instance for a given message class, and
 | 
				
			||||||
 | 
						 * generates a new implementation if none exists yet.
 | 
				
			||||||
 | 
						 * @param messageClass The class of the message to get a type for.
 | 
				
			||||||
 | 
						 * @param <T> The type of the message.
 | 
				
			||||||
 | 
						 * @return The message type.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@SuppressWarnings("unchecked")
 | 
				
			||||||
 | 
						public static <T extends Message> MessageType<T> get(Class<T> messageClass) {
 | 
				
			||||||
 | 
							return (MessageType<T>) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class<T>) c));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Generates a message type instance for a given class, using reflection to
 | 
				
			||||||
 | 
						 * introspect the fields of the message.
 | 
				
			||||||
 | 
						 * <p>
 | 
				
			||||||
 | 
						 *     Note that this only works for record-based messages.
 | 
				
			||||||
 | 
						 * </p>
 | 
				
			||||||
 | 
						 * @param messageTypeClass The class of the message type.
 | 
				
			||||||
 | 
						 * @param <T> The type of the message.
 | 
				
			||||||
 | 
						 * @return A message type instance.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static <T extends Message> MessageType<T> generateForRecord(Class<T> messageTypeClass) {
 | 
				
			||||||
 | 
							RecordComponent[] components = messageTypeClass.getRecordComponents();
 | 
				
			||||||
 | 
							Constructor<T> constructor;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								constructor = messageTypeClass.getDeclaredConstructor(Arrays.stream(components)
 | 
				
			||||||
 | 
										.map(RecordComponent::getType).toArray(Class<?>[]::new));
 | 
				
			||||||
 | 
							} catch (NoSuchMethodException e) {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return new MessageType<>(
 | 
				
			||||||
 | 
									messageTypeClass,
 | 
				
			||||||
 | 
									generateByteSizeFunction(components),
 | 
				
			||||||
 | 
									generateReader(constructor),
 | 
				
			||||||
 | 
									generateWriter(components)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Generates a function implementation that counts the byte size of a
 | 
				
			||||||
 | 
						 * message based on the message's record component types.
 | 
				
			||||||
 | 
						 * @param components The list of components that make up the message.
 | 
				
			||||||
 | 
						 * @param <T> The message type.
 | 
				
			||||||
 | 
						 * @return A function that computes the byte size of a message of the given
 | 
				
			||||||
 | 
						 * type.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private static <T extends Message> Function<T, Integer> generateByteSizeFunction(RecordComponent[] components) {
 | 
				
			||||||
 | 
							return msg -> {
 | 
				
			||||||
 | 
								int size = 0;
 | 
				
			||||||
 | 
								for (var component : components) {
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										size += MessageUtils.getByteSize(component.getAccessor().invoke(msg));
 | 
				
			||||||
 | 
									} catch (ReflectiveOperationException e) {
 | 
				
			||||||
 | 
										throw new IllegalStateException(e);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return size;
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Generates a message reader for the given message constructor method. It
 | 
				
			||||||
 | 
						 * will try to read objects from the input stream according to the
 | 
				
			||||||
 | 
						 * parameters of the canonical constructor of a message record.
 | 
				
			||||||
 | 
						 * @param constructor The canonical constructor of the message record.
 | 
				
			||||||
 | 
						 * @param <T> The message type.
 | 
				
			||||||
 | 
						 * @return A message reader for the given type.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private static <T extends Message> MessageReader<T> generateReader(Constructor<T> constructor) {
 | 
				
			||||||
 | 
							return in -> {
 | 
				
			||||||
 | 
								Object[] values = new Object[constructor.getParameterCount()];
 | 
				
			||||||
 | 
								for (int i = 0; i < values.length; i++) {
 | 
				
			||||||
 | 
									values[i] = in.readObject(constructor.getParameterTypes()[i]);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									return constructor.newInstance(values);
 | 
				
			||||||
 | 
								} catch (ReflectiveOperationException e) {
 | 
				
			||||||
 | 
									throw new IllegalStateException(e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Generates a message writer for the given message record components.
 | 
				
			||||||
 | 
						 * @param components The record components to write.
 | 
				
			||||||
 | 
						 * @param <T> The type of message.
 | 
				
			||||||
 | 
						 * @return The message writer for the given type.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private static <T extends Message> MessageWriter<T> generateWriter(RecordComponent[] components) {
 | 
				
			||||||
 | 
							return (msg, out) -> {
 | 
				
			||||||
 | 
								for (var component: components) {
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										out.writeObject(component.getAccessor().invoke(msg), component.getType());
 | 
				
			||||||
 | 
									} catch (ReflectiveOperationException e) {
 | 
				
			||||||
 | 
										throw new IllegalStateException(e);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,6 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg;
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.nio.charset.StandardCharsets;
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
import java.util.ArrayList;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -14,6 +9,7 @@ import java.util.UUID;
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class MessageUtils {
 | 
					public class MessageUtils {
 | 
				
			||||||
	public static final int UUID_BYTES = 2 * Long.BYTES;
 | 
						public static final int UUID_BYTES = 2 * Long.BYTES;
 | 
				
			||||||
 | 
						public static final int ENUM_BYTES = Integer.BYTES;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * 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
 | 
				
			||||||
| 
						 | 
					@ -26,118 +22,54 @@ public class MessageUtils {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Writes a string to the given output stream using a length-prefixed format
 | 
						 * Gets the number of bytes that all the given strings will occupy when
 | 
				
			||||||
	 * where an integer length precedes the string's bytes, which are encoded in
 | 
						 * serialized with a length-prefix encoding.
 | 
				
			||||||
	 * UTF-8.
 | 
						 * @param strings The set of strings.
 | 
				
			||||||
	 * @param s The string to write.
 | 
						 * @return The total byte size.
 | 
				
			||||||
	 * @param o The output stream to write to.
 | 
					 | 
				
			||||||
	 * @throws IOException If the stream could not be written to.
 | 
					 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static void writeString(String s, DataOutputStream o) throws IOException {
 | 
						public static int getByteSize(String... strings) {
 | 
				
			||||||
		if (s == null) {
 | 
							int size = 0;
 | 
				
			||||||
			o.writeInt(-1);
 | 
							for (var s : strings) {
 | 
				
			||||||
		} else {
 | 
								size += getByteSize(s);
 | 
				
			||||||
			o.writeInt(s.length());
 | 
					 | 
				
			||||||
			o.write(s.getBytes(StandardCharsets.UTF_8));
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							return size;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						public static <T extends Message> int getByteSize(T[] items) {
 | 
				
			||||||
	 * Reads a string from the given input stream, using a length-prefixed
 | 
					 | 
				
			||||||
	 * format, where an integer length precedes the string's bytes, which are
 | 
					 | 
				
			||||||
	 * encoded in UTF-8.
 | 
					 | 
				
			||||||
	 * @param i The input stream to read from.
 | 
					 | 
				
			||||||
	 * @return The string which was read.
 | 
					 | 
				
			||||||
	 * @throws IOException If the stream could not be read, or if the string is
 | 
					 | 
				
			||||||
	 * malformed.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public static String readString(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		int length = i.readInt();
 | 
					 | 
				
			||||||
		if (length == -1) return null;
 | 
					 | 
				
			||||||
		if (length == 0) return "";
 | 
					 | 
				
			||||||
		byte[] data = new byte[length];
 | 
					 | 
				
			||||||
		int read = i.read(data);
 | 
					 | 
				
			||||||
		if (read != length) throw new IOException("Not all bytes of a string of length " + length + " could be read.");
 | 
					 | 
				
			||||||
		return new String(data, StandardCharsets.UTF_8);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Writes an enum value to the given stream as the integer ordinal value of
 | 
					 | 
				
			||||||
	 * the enum value, or -1 if the value is null.
 | 
					 | 
				
			||||||
	 * @param value The value to write.
 | 
					 | 
				
			||||||
	 * @param o The output stream.
 | 
					 | 
				
			||||||
	 * @throws IOException If an error occurs while writing.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public static void writeEnum(Enum<?> value, DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		if (value == null) {
 | 
					 | 
				
			||||||
			o.writeInt(-1);
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			o.writeInt(value.ordinal());
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Reads an enum value from the given stream, assuming that the value is
 | 
					 | 
				
			||||||
	 * represented by an integer ordinal value.
 | 
					 | 
				
			||||||
	 * @param e The type of enum that is to be read.
 | 
					 | 
				
			||||||
	 * @param i The input stream to read from.
 | 
					 | 
				
			||||||
	 * @param <T> The enum type.
 | 
					 | 
				
			||||||
	 * @return The enum value, or null if -1 was read.
 | 
					 | 
				
			||||||
	 * @throws IOException If an error occurs while reading.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public static <T extends Enum<?>> T readEnum(Class<T> e, DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		int ordinal = i.readInt();
 | 
					 | 
				
			||||||
		if (ordinal == -1) return null;
 | 
					 | 
				
			||||||
		return e.getEnumConstants()[ordinal];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static void writeUUID(UUID value, DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		if (value == null) {
 | 
					 | 
				
			||||||
			o.writeLong(-1);
 | 
					 | 
				
			||||||
			o.writeLong(-1);
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			o.writeLong(value.getMostSignificantBits());
 | 
					 | 
				
			||||||
			o.writeLong(value.getLeastSignificantBits());
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static UUID readUUID(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		long a = i.readLong();
 | 
					 | 
				
			||||||
		long b = i.readLong();
 | 
					 | 
				
			||||||
		if (a == -1 && b == -1) {
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return new UUID(a, b);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static int getByteSize(List<? extends Message> items) {
 | 
					 | 
				
			||||||
		int count = Integer.BYTES;
 | 
							int count = Integer.BYTES;
 | 
				
			||||||
		for (var item : items) {
 | 
							for (var item : items) {
 | 
				
			||||||
			count += item.getByteCount();
 | 
								count += item.byteSize();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return count;
 | 
							return count;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static void writeList(List<? extends Message> items, DataOutputStream o) throws IOException {
 | 
						public static int getByteSize(Object o) {
 | 
				
			||||||
		o.writeInt(items.size());
 | 
							if (o instanceof Integer) {
 | 
				
			||||||
		for (var i : items) {
 | 
								return Integer.BYTES;
 | 
				
			||||||
			i.write(o);
 | 
							} else if (o instanceof Long) {
 | 
				
			||||||
 | 
								return Long.BYTES;
 | 
				
			||||||
 | 
							} else if (o instanceof String) {
 | 
				
			||||||
 | 
								return getByteSize((String) o);
 | 
				
			||||||
 | 
							} else if (o instanceof UUID) {
 | 
				
			||||||
 | 
								return UUID_BYTES;
 | 
				
			||||||
 | 
							} else if (o instanceof Enum<?>) {
 | 
				
			||||||
 | 
								return ENUM_BYTES;
 | 
				
			||||||
 | 
							} else if (o instanceof byte[]) {
 | 
				
			||||||
 | 
								return Integer.BYTES + ((byte[]) o).length;
 | 
				
			||||||
 | 
							} else if (o.getClass().isArray() && Message.class.isAssignableFrom(o.getClass().getComponentType())) {
 | 
				
			||||||
 | 
								return getByteSize((Message[]) o);
 | 
				
			||||||
 | 
							} else if (o instanceof Message) {
 | 
				
			||||||
 | 
								return ((Message) o).byteSize();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException("Unsupported object type: " + o.getClass().getSimpleName());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static <T extends Message> List<T> readList(Class<T> type, DataInputStream i) throws IOException {
 | 
						public static int getByteSize(Object... objects) {
 | 
				
			||||||
		int size = i.readInt();
 | 
							int size = 0;
 | 
				
			||||||
		try {
 | 
							for (var o : objects) {
 | 
				
			||||||
			var constructor = type.getConstructor();
 | 
								size += getByteSize(o);
 | 
				
			||||||
			List<T> items = new ArrayList<>(size);
 | 
					 | 
				
			||||||
			for (int k = 0; k < size; k++) {
 | 
					 | 
				
			||||||
				var item = constructor.newInstance();
 | 
					 | 
				
			||||||
				item.read(i);
 | 
					 | 
				
			||||||
				items.add(item);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return items;
 | 
					 | 
				
			||||||
		} catch (ReflectiveOperationException e) {
 | 
					 | 
				
			||||||
			throw new IOException(e);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							return size;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.util.ChainedDataOutputStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@FunctionalInterface
 | 
				
			||||||
 | 
					public interface MessageWriter<T extends Message> {
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Writes this message to the given output stream.
 | 
				
			||||||
 | 
						 * @param msg The message to write.
 | 
				
			||||||
 | 
						 * @param out The output stream to write to.
 | 
				
			||||||
 | 
						 * @throws IOException If an error occurs while writing.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						void write(T msg, ChainedDataOutputStream out) throws IOException;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,24 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg;
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Error;
 | 
					import nl.andrewl.concord_core.msg.types.Error;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.*;
 | 
					import nl.andrewl.concord_core.msg.types.ServerMetaData;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.ServerUsers;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.channel.CreateThread;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.KeyData;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.Registration;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.util.ChainedDataOutputStream;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.util.ExtendedDataInputStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.*;
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.io.OutputStream;
 | 
				
			||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,13 +32,13 @@ public class Serializer {
 | 
				
			||||||
	 * The mapping which defines each supported message type and the byte value
 | 
						 * The mapping which defines each supported message type and the byte value
 | 
				
			||||||
	 * used to identify it when reading and writing messages.
 | 
						 * used to identify it when reading and writing messages.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	private final Map<Byte, Class<? extends Message>> messageTypes = new HashMap<>();
 | 
						private final Map<Byte, MessageType<?>> messageTypes = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * An inverse of {@link Serializer#messageTypes} which is used to look up a
 | 
						 * An inverse of {@link Serializer#messageTypes} which is used to look up a
 | 
				
			||||||
	 * message's byte value when you know the class of the message.
 | 
						 * message's byte value when you know the class of the message.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	private final Map<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>();
 | 
						private final Map<MessageType<?>, Byte> inverseMessageTypes = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Constructs a new serializer instance, with a standard set of supported
 | 
						 * Constructs a new serializer instance, with a standard set of supported
 | 
				
			||||||
| 
						 | 
					@ -36,7 +51,7 @@ public class Serializer {
 | 
				
			||||||
		registerType(3, MoveToChannel.class);
 | 
							registerType(3, MoveToChannel.class);
 | 
				
			||||||
		registerType(4, ChatHistoryRequest.class);
 | 
							registerType(4, ChatHistoryRequest.class);
 | 
				
			||||||
		registerType(5, ChatHistoryResponse.class);
 | 
							registerType(5, ChatHistoryResponse.class);
 | 
				
			||||||
		// Type id 6 removed due to deprecation.
 | 
							registerType(6, Registration.class);
 | 
				
			||||||
		registerType(7, ServerUsers.class);
 | 
							registerType(7, ServerUsers.class);
 | 
				
			||||||
		registerType(8, ServerMetaData.class);
 | 
							registerType(8, ServerMetaData.class);
 | 
				
			||||||
		registerType(9, Error.class);
 | 
							registerType(9, Error.class);
 | 
				
			||||||
| 
						 | 
					@ -49,12 +64,12 @@ public class Serializer {
 | 
				
			||||||
	 * serializer, by adding it to the normal and inverse mappings.
 | 
						 * serializer, by adding it to the normal and inverse mappings.
 | 
				
			||||||
	 * @param id The byte which will be used to identify messages of the given
 | 
						 * @param id The byte which will be used to identify messages of the given
 | 
				
			||||||
	 *           class. The value should from 0 to 127.
 | 
						 *           class. The value should from 0 to 127.
 | 
				
			||||||
	 * @param messageClass The class of message which is registered with the
 | 
						 * @param messageClass The type of message associated with the given id.
 | 
				
			||||||
	 *                     given byte identifier.
 | 
					 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	private synchronized void registerType(int id, Class<? extends Message> messageClass) {
 | 
						private synchronized <T extends Message> void registerType(int id, Class<T> messageClass) {
 | 
				
			||||||
		messageTypes.put((byte) id, messageClass);
 | 
							MessageType<T> type = MessageType.get(messageClass);
 | 
				
			||||||
		inverseMessageTypes.put(messageClass, (byte) id);
 | 
							messageTypes.put((byte) id, type);
 | 
				
			||||||
 | 
							inverseMessageTypes.put(type, (byte) id);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -67,19 +82,16 @@ public class Serializer {
 | 
				
			||||||
	 * constructed for the incoming data.
 | 
						 * constructed for the incoming data.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public Message readMessage(InputStream i) throws IOException {
 | 
						public Message readMessage(InputStream i) throws IOException {
 | 
				
			||||||
		DataInputStream d = new DataInputStream(i);
 | 
							ExtendedDataInputStream d = new ExtendedDataInputStream(i);
 | 
				
			||||||
		byte type = d.readByte();
 | 
							byte typeId = d.readByte();
 | 
				
			||||||
		var clazz = messageTypes.get(type);
 | 
							var type = messageTypes.get(typeId);
 | 
				
			||||||
		if (clazz == null) {
 | 
							if (type == null) {
 | 
				
			||||||
			throw new IOException("Unsupported message type: " + type);
 | 
								throw new IOException("Unsupported message type: " + typeId);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			var constructor = clazz.getConstructor();
 | 
								return type.reader().read(d);
 | 
				
			||||||
			var message = constructor.newInstance();
 | 
					 | 
				
			||||||
			message.read(d);
 | 
					 | 
				
			||||||
			return message;
 | 
					 | 
				
			||||||
		} catch (Throwable e) {
 | 
							} catch (Throwable e) {
 | 
				
			||||||
			throw new IOException("Could not instantiate new message object of type " + clazz.getSimpleName(), e);
 | 
								throw new IOException("Could not instantiate new message object of type " + type.getClass().getSimpleName(), e);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,14 +102,14 @@ public class Serializer {
 | 
				
			||||||
	 * @throws IOException If an error occurs while writing, or if the message
 | 
						 * @throws IOException If an error occurs while writing, or if the message
 | 
				
			||||||
	 * to write is not supported by this serializer.
 | 
						 * to write is not supported by this serializer.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void writeMessage(Message msg, OutputStream o) throws IOException {
 | 
						public <T extends Message> void writeMessage(Message msg, OutputStream o) throws IOException {
 | 
				
			||||||
		DataOutputStream d = new DataOutputStream(o);
 | 
							DataOutputStream d = new DataOutputStream(o);
 | 
				
			||||||
		Byte type = inverseMessageTypes.get(msg.getClass());
 | 
							Byte typeId = inverseMessageTypes.get(msg.getType());
 | 
				
			||||||
		if (type == null) {
 | 
							if (typeId == null) {
 | 
				
			||||||
			throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
 | 
								throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		d.writeByte(type);
 | 
							d.writeByte(typeId);
 | 
				
			||||||
		msg.write(d);
 | 
							msg.getType().writer().write(msg, new ChainedDataOutputStream(d));
 | 
				
			||||||
		d.flush();
 | 
							d.flush();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,83 +0,0 @@
 | 
				
			||||||
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.Objects;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This message contains information about a chat message that a user sent.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Data
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
public class Chat implements Message {
 | 
					 | 
				
			||||||
	private static final long ID_NONE = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private UUID id;
 | 
					 | 
				
			||||||
	private UUID senderId;
 | 
					 | 
				
			||||||
	private String senderNickname;
 | 
					 | 
				
			||||||
	private long timestamp;
 | 
					 | 
				
			||||||
	private String message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
 | 
					 | 
				
			||||||
		this.id = null;
 | 
					 | 
				
			||||||
		this.senderId = senderId;
 | 
					 | 
				
			||||||
		this.senderNickname = senderNickname;
 | 
					 | 
				
			||||||
		this.timestamp = timestamp;
 | 
					 | 
				
			||||||
		this.message = message;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Chat(String message) {
 | 
					 | 
				
			||||||
		this(null, null, System.currentTimeMillis(), message);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return 2 * UUID_BYTES + Long.BYTES + getByteSize(this.senderNickname) + getByteSize(this.message);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeUUID(this.id, o);
 | 
					 | 
				
			||||||
		writeUUID(this.senderId, o);
 | 
					 | 
				
			||||||
		writeString(this.senderNickname, o);
 | 
					 | 
				
			||||||
		o.writeLong(this.timestamp);
 | 
					 | 
				
			||||||
		writeString(this.message, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.id = readUUID(i);
 | 
					 | 
				
			||||||
		this.senderId = readUUID(i);
 | 
					 | 
				
			||||||
		this.senderNickname = readString(i);
 | 
					 | 
				
			||||||
		this.timestamp = i.readLong();
 | 
					 | 
				
			||||||
		this.message = readString(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public String toString() {
 | 
					 | 
				
			||||||
		return String.format("%s: %s", this.senderNickname, this.message);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public boolean equals(Object o) {
 | 
					 | 
				
			||||||
		if (o.getClass().equals(this.getClass())) {
 | 
					 | 
				
			||||||
			Chat other = (Chat) o;
 | 
					 | 
				
			||||||
			if (Objects.equals(this.getId(), other.getId())) return true;
 | 
					 | 
				
			||||||
			return this.getSenderId().equals(other.getSenderId()) &&
 | 
					 | 
				
			||||||
					this.getTimestamp() == other.getTimestamp() &&
 | 
					 | 
				
			||||||
					this.getSenderNickname().equals(other.getSenderNickname()) &&
 | 
					 | 
				
			||||||
					this.message.length() == other.message.length();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return false;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,43 +0,0 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * The response that a server sends to a {@link ChatHistoryRequest}. The list of
 | 
					 | 
				
			||||||
 * messages is ordered by timestamp, with the newest messages appearing first.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Data
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
public class ChatHistoryResponse implements Message {
 | 
					 | 
				
			||||||
	private UUID channelId;
 | 
					 | 
				
			||||||
	List<Chat> messages;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return UUID_BYTES + getByteSize(messages);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeUUID(this.channelId, o);
 | 
					 | 
				
			||||||
		writeList(this.messages, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.channelId = readUUID(i);
 | 
					 | 
				
			||||||
		this.messages = readList(Chat.class, i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,56 +0,0 @@
 | 
				
			||||||
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,24 +1,15 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Error message which can be sent between either the server or client to
 | 
					 * Error message which can be sent between either the server or client to
 | 
				
			||||||
 * indicate an unsavory situation.
 | 
					 * indicate an unsavory situation.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Data
 | 
					public record Error (
 | 
				
			||||||
@NoArgsConstructor
 | 
							Level level,
 | 
				
			||||||
@AllArgsConstructor
 | 
							String message
 | 
				
			||||||
public class Error implements Message {
 | 
					) implements Message {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The error level gives an indication as to the severity of the error.
 | 
						 * The error level gives an indication as to the severity of the error.
 | 
				
			||||||
	 * Warnings indicate that a user has attempted to do something which they
 | 
						 * Warnings indicate that a user has attempted to do something which they
 | 
				
			||||||
| 
						 | 
					@ -27,9 +18,6 @@ public class Error implements Message {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public enum Level {WARNING, ERROR}
 | 
						public enum Level {WARNING, ERROR}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private Level level;
 | 
					 | 
				
			||||||
	private String message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static Error warning(String message) {
 | 
						public static Error warning(String message) {
 | 
				
			||||||
		return new Error(Level.WARNING, message);
 | 
							return new Error(Level.WARNING, message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -37,21 +25,4 @@ public class Error implements Message {
 | 
				
			||||||
	public static Error error(String message) {
 | 
						public static Error error(String message) {
 | 
				
			||||||
		return new Error(Level.ERROR, message);
 | 
							return new Error(Level.ERROR, message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return Integer.BYTES + getByteSize(this.message);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeEnum(this.level, o);
 | 
					 | 
				
			||||||
		writeString(this.message, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.level = readEnum(Level.class, i);
 | 
					 | 
				
			||||||
		this.message = readString(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,56 +0,0 @@
 | 
				
			||||||
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 static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This message is sent from the client to a server, to provide identification
 | 
					 | 
				
			||||||
 * information about the client to the server when the connection is started.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Data
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
public class Identification implements Message {
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * The nickname that a client wants to be identified by when in the server.
 | 
					 | 
				
			||||||
	 * If a valid session token is provided, this can be left as null, and the
 | 
					 | 
				
			||||||
	 * user will be given the same nickname they had in their previous session.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private String nickname;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * A session token that's used to uniquely identify this client as the same
 | 
					 | 
				
			||||||
	 * as one who has previously connected to the server. If this is null, the
 | 
					 | 
				
			||||||
	 * client is indicating that they have not connected to this server before.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private String sessionToken;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Identification(String nickname) {
 | 
					 | 
				
			||||||
		this.nickname = nickname;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return getByteSize(this.nickname) + getByteSize(sessionToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeString(this.nickname, o);
 | 
					 | 
				
			||||||
		writeString(this.sessionToken, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.nickname = readString(i);
 | 
					 | 
				
			||||||
		this.sessionToken = readString(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,52 +0,0 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lombok.Getter;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This message is sent as the first message from both the server and the client
 | 
					 | 
				
			||||||
 * to establish an end-to-end encryption via a key exchange.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Getter
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
public class KeyData implements Message {
 | 
					 | 
				
			||||||
	private byte[] iv;
 | 
					 | 
				
			||||||
	private byte[] salt;
 | 
					 | 
				
			||||||
	private byte[] publicKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public KeyData(byte[] iv, byte[] salt, byte[] publicKey) {
 | 
					 | 
				
			||||||
		this.iv = iv;
 | 
					 | 
				
			||||||
		this.salt = salt;
 | 
					 | 
				
			||||||
		this.publicKey = publicKey;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return Integer.BYTES * 3 + iv.length + salt.length + publicKey.length;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		o.writeInt(iv.length);
 | 
					 | 
				
			||||||
		o.write(iv);
 | 
					 | 
				
			||||||
		o.writeInt(salt.length);
 | 
					 | 
				
			||||||
		o.write(salt);
 | 
					 | 
				
			||||||
		o.writeInt(publicKey.length);
 | 
					 | 
				
			||||||
		o.write(publicKey);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		int ivLength = i.readInt();
 | 
					 | 
				
			||||||
		this.iv = i.readNBytes(ivLength);
 | 
					 | 
				
			||||||
		int saltLength = i.readInt();
 | 
					 | 
				
			||||||
		this.salt = i.readNBytes(saltLength);
 | 
					 | 
				
			||||||
		int publicKeyLength = i.readInt();
 | 
					 | 
				
			||||||
		this.publicKey = i.readNBytes(publicKeyLength);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,67 +0,0 @@
 | 
				
			||||||
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.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * A message that's sent to a client when they've been moved to another channel.
 | 
					 | 
				
			||||||
 * This indicates to the client that they should perform the necessary requests
 | 
					 | 
				
			||||||
 * to update their view to indicate that they're now in a different channel.
 | 
					 | 
				
			||||||
 * <p>
 | 
					 | 
				
			||||||
 *     Conversely, a client can send this request to the server to indicate that
 | 
					 | 
				
			||||||
 *     they would like to switch to the specified channel.
 | 
					 | 
				
			||||||
 * </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
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
public class MoveToChannel implements Message {
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * 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) {
 | 
					 | 
				
			||||||
		this.id = channelId;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return UUID_BYTES + getByteSize(this.channelName);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeUUID(this.id, o);
 | 
					 | 
				
			||||||
		writeString(this.channelName, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.id = readUUID(i);
 | 
					 | 
				
			||||||
		this.channelName = readString(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,73 +1,18 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Metadata is sent by the server to clients to inform them of the structure of
 | 
					 * 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
 | 
					 * the server. This includes basic information about the server's own properties
 | 
				
			||||||
 * as well as information about all top-level channels.
 | 
					 * as well as information about all top-level channels.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Data
 | 
					public record ServerMetaData (String name, ChannelData[] channels) implements Message {
 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
public class ServerMetaData implements Message {
 | 
					 | 
				
			||||||
	private String name;
 | 
					 | 
				
			||||||
	private List<ChannelData> channels;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return getByteSize(this.name) + getByteSize(this.channels);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeString(this.name, o);
 | 
					 | 
				
			||||||
		writeList(this.channels, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.name = readString(i);
 | 
					 | 
				
			||||||
		this.channels = readList(ChannelData.class, i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Metadata about a top-level channel in the server which is visible and
 | 
						 * Metadata about a top-level channel in the server which is visible and
 | 
				
			||||||
	 * joinable for a user.
 | 
						 * joinable for a user.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@Data
 | 
						public static record ChannelData (UUID id, String name) implements Message {}
 | 
				
			||||||
	@NoArgsConstructor
 | 
					 | 
				
			||||||
	@AllArgsConstructor
 | 
					 | 
				
			||||||
	public static class ChannelData implements Message {
 | 
					 | 
				
			||||||
		private UUID id;
 | 
					 | 
				
			||||||
		private String name;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		@Override
 | 
					 | 
				
			||||||
		public int getByteCount() {
 | 
					 | 
				
			||||||
			return UUID_BYTES + getByteSize(this.name);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		@Override
 | 
					 | 
				
			||||||
		public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
			writeUUID(this.id, o);
 | 
					 | 
				
			||||||
			writeString(this.name, o);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		@Override
 | 
					 | 
				
			||||||
		public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
			this.id = readUUID(i);
 | 
					 | 
				
			||||||
			this.name = readString(i);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,40 +1,10 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					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
 | 
					 * 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
 | 
					 * which requires the server to notify clients about a change of the list of
 | 
				
			||||||
 * global users.
 | 
					 * global users.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Data
 | 
					public record ServerUsers (UserData[] users) implements Message {}
 | 
				
			||||||
@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);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,72 +0,0 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This message is sent from the server to the client after the server accepts
 | 
					 | 
				
			||||||
 * the client's identification and registers the client in the server.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Data
 | 
					 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
public class ServerWelcome implements Message {
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * The unique id of this client.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private UUID clientId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * The token which this client can use to reconnect to the server later and
 | 
					 | 
				
			||||||
	 * still be recognized as the same user.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private String sessionToken;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * The id of the channel that the user has been placed in.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private UUID currentChannelId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * The name of the channel that the user has been placed in.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private String currentChannelName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * Information about the server's structure.
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	private ServerMetaData metaData;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return 2 * UUID_BYTES + getByteSize(this.sessionToken) + getByteSize(this.currentChannelName) + this.metaData.getByteCount();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeUUID(this.clientId, o);
 | 
					 | 
				
			||||||
		writeString(this.sessionToken, o);
 | 
					 | 
				
			||||||
		writeUUID(this.currentChannelId, o);
 | 
					 | 
				
			||||||
		writeString(this.currentChannelName, o);
 | 
					 | 
				
			||||||
		this.metaData.write(o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.clientId = readUUID(i);
 | 
					 | 
				
			||||||
		this.sessionToken = readString(i);
 | 
					 | 
				
			||||||
		this.currentChannelId = readUUID(i);
 | 
					 | 
				
			||||||
		this.metaData = new ServerMetaData();
 | 
					 | 
				
			||||||
		this.currentChannelName = readString(i);
 | 
					 | 
				
			||||||
		this.metaData.read(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,43 +1,11 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					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
 | 
					 * Standard set of user data that is used mainly as a component of other more
 | 
				
			||||||
 * complex messages.
 | 
					 * complex messages.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Data
 | 
					public record UserData (UUID id, String name) implements Message {}
 | 
				
			||||||
@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);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.channel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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>
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param messageId The id of the message that a thread will be/is attached to.
 | 
				
			||||||
 | 
					 * @param title The title of the thread. This may be null.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record CreateThread (UUID messageId, String title) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.channel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A message that's sent to a client when they've been moved to another channel.
 | 
				
			||||||
 | 
					 * This indicates to the client that they should perform the necessary requests
 | 
				
			||||||
 | 
					 * to update their view to indicate that they're now in a different channel.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     Conversely, a client can send this request to the server to indicate that
 | 
				
			||||||
 | 
					 *     they would like to switch to the specified channel.
 | 
				
			||||||
 | 
					 * </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>
 | 
				
			||||||
 | 
					 * @param id 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.
 | 
				
			||||||
 | 
					 * @param channelName 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.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record MoveToChannel (UUID id, String channelName) implements Message {
 | 
				
			||||||
 | 
						public MoveToChannel(UUID id) {
 | 
				
			||||||
 | 
							this(id, null);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This message contains information about a chat message that a user sent.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record Chat (
 | 
				
			||||||
 | 
							UUID id, UUID senderId, String senderNickname, long timestamp, String message
 | 
				
			||||||
 | 
					) implements Message {
 | 
				
			||||||
 | 
						public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
 | 
				
			||||||
 | 
							this(null, senderId, senderNickname, timestamp, message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Chat(String message) {
 | 
				
			||||||
 | 
							this(null, null, System.currentTimeMillis(), message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Chat(UUID newId, Chat original) {
 | 
				
			||||||
 | 
							this(newId, original.senderId, original.senderNickname, original.timestamp, original.message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public String toString() {
 | 
				
			||||||
 | 
							return String.format("%s: %s", this.senderNickname, this.message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public boolean equals(Object o) {
 | 
				
			||||||
 | 
							if (o.getClass().equals(this.getClass())) {
 | 
				
			||||||
 | 
								Chat other = (Chat) o;
 | 
				
			||||||
 | 
								if (Objects.equals(this.id, other.id)) return true;
 | 
				
			||||||
 | 
								return this.senderId.equals(other.senderId) &&
 | 
				
			||||||
 | 
										this.timestamp == other.timestamp &&
 | 
				
			||||||
 | 
										this.senderNickname.equals(other.senderNickname) &&
 | 
				
			||||||
 | 
										this.message.length() == other.message.length();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,12 @@
 | 
				
			||||||
package nl.andrewl.concord_core.msg.types;
 | 
					package nl.andrewl.concord_core.msg.types.chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.Data;
 | 
					 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.DataInputStream;
 | 
					 | 
				
			||||||
import java.io.DataOutputStream;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * A message which clients can send to the server to request some messages from
 | 
					 * A message which clients can send to the server to request some messages from
 | 
				
			||||||
 * the server's history of all sent messages from a particular source. Every
 | 
					 * the server's history of all sent messages from a particular source. Every
 | 
				
			||||||
| 
						 | 
					@ -51,20 +43,15 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
 | 
				
			||||||
 *     the list of messages is always sorted by the timestamp.
 | 
					 *     the list of messages is always sorted by the timestamp.
 | 
				
			||||||
 * </p>
 | 
					 * </p>
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Data
 | 
					public record ChatHistoryRequest (UUID channelId, String query) implements Message {
 | 
				
			||||||
@NoArgsConstructor
 | 
					 | 
				
			||||||
@AllArgsConstructor
 | 
					 | 
				
			||||||
public class ChatHistoryRequest implements Message {
 | 
					 | 
				
			||||||
	private UUID channelId;
 | 
					 | 
				
			||||||
	private String query;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public ChatHistoryRequest(UUID channelId) {
 | 
						public ChatHistoryRequest(UUID channelId) {
 | 
				
			||||||
		this(channelId, "");
 | 
							this(channelId, "");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public ChatHistoryRequest(UUID channelId, Map<String, String> params) {
 | 
						public ChatHistoryRequest(UUID channelId, Map<String, String> params) {
 | 
				
			||||||
		this.channelId = channelId;
 | 
							this(
 | 
				
			||||||
		this.query = params.entrySet().stream()
 | 
								channelId,
 | 
				
			||||||
 | 
								params.entrySet().stream()
 | 
				
			||||||
				.map(entry -> {
 | 
									.map(entry -> {
 | 
				
			||||||
					if (entry.getKey().contains(";") || entry.getKey().contains("=")) {
 | 
										if (entry.getKey().contains(";") || entry.getKey().contains("=")) {
 | 
				
			||||||
						throw new IllegalArgumentException("Parameter key \"" + entry.getKey() + "\" contains invalid characters.");
 | 
											throw new IllegalArgumentException("Parameter key \"" + entry.getKey() + "\" contains invalid characters.");
 | 
				
			||||||
| 
						 | 
					@ -74,7 +61,8 @@ public class ChatHistoryRequest implements Message {
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					return entry.getKey() + "=" + entry.getValue();
 | 
										return entry.getKey() + "=" + entry.getValue();
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				.collect(Collectors.joining(";"));
 | 
									.collect(Collectors.joining(";"))
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -92,21 +80,4 @@ public class ChatHistoryRequest implements Message {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return params;
 | 
							return params;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int getByteCount() {
 | 
					 | 
				
			||||||
		return UUID_BYTES + getByteSize(this.query);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void write(DataOutputStream o) throws IOException {
 | 
					 | 
				
			||||||
		writeUUID(this.channelId, o);
 | 
					 | 
				
			||||||
		writeString(this.query, o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public void read(DataInputStream i) throws IOException {
 | 
					 | 
				
			||||||
		this.channelId = readUUID(i);
 | 
					 | 
				
			||||||
		this.query = readString(i);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The response that a server sends to a {@link ChatHistoryRequest}. The list of
 | 
				
			||||||
 | 
					 * messages is ordered by timestamp, with the newest messages appearing first.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record ChatHistoryResponse (UUID channelId, Chat[] messages) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.client_setup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This message is sent from the client to a server, to provide identification
 | 
				
			||||||
 | 
					 * information about the client to the server when the connection is started.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param nickname
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record Identification(String nickname, String sessionToken) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.client_setup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This message is sent as the first message from both the server and the client
 | 
				
			||||||
 | 
					 * to establish an end-to-end encryption via a key exchange.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record KeyData (byte[] iv, byte[] salt, byte[] publicKey) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.client_setup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The data that new users should send to a server in order to register in that
 | 
				
			||||||
 | 
					 * server.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record Registration (String username, String password) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.client_setup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.ServerMetaData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This message is sent from the server to the client after the server accepts
 | 
				
			||||||
 | 
					 * the client's identification and registers the client in the server.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param clientId The unique id of this client.
 | 
				
			||||||
 | 
					 * @param sessionToken The token which this client can use to reconnect to the
 | 
				
			||||||
 | 
					 *                     server later and still be recognized as the same user.
 | 
				
			||||||
 | 
					 * @param currentChannelId The id of the channel that the user is placed in.
 | 
				
			||||||
 | 
					 * @param currentChannelName The name of the channel that the user is placed in.
 | 
				
			||||||
 | 
					 * @param metaData Information about the server's structure.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record ServerWelcome (
 | 
				
			||||||
 | 
							UUID clientId,
 | 
				
			||||||
 | 
							String sessionToken,
 | 
				
			||||||
 | 
							UUID currentChannelId,
 | 
				
			||||||
 | 
							String currentChannelName,
 | 
				
			||||||
 | 
							ServerMetaData metaData
 | 
				
			||||||
 | 
					) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,108 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A more complex output stream which redefines certain methods for convenience
 | 
				
			||||||
 | 
					 * with method chaining.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ChainedDataOutputStream {
 | 
				
			||||||
 | 
						private final DataOutputStream out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream(DataOutputStream out) {
 | 
				
			||||||
 | 
							this.out = out;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeInt(int x) throws IOException {
 | 
				
			||||||
 | 
							out.writeInt(x);
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeString(String s) throws IOException {
 | 
				
			||||||
 | 
							if (s == null) {
 | 
				
			||||||
 | 
								out.writeInt(-1);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								out.writeInt(s.length());
 | 
				
			||||||
 | 
								out.write(s.getBytes(StandardCharsets.UTF_8));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeStrings(String... strings) throws IOException {
 | 
				
			||||||
 | 
							for (var s : strings) {
 | 
				
			||||||
 | 
								writeString(s);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeEnum(Enum<?> value) throws IOException {
 | 
				
			||||||
 | 
							if (value == null) {
 | 
				
			||||||
 | 
								out.writeInt(-1);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								out.writeInt(value.ordinal());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeUUID(UUID uuid) throws IOException {
 | 
				
			||||||
 | 
							if (uuid == null) {
 | 
				
			||||||
 | 
								out.writeLong(-1);
 | 
				
			||||||
 | 
								out.writeLong(-1);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								out.writeLong(uuid.getMostSignificantBits());
 | 
				
			||||||
 | 
								out.writeLong(uuid.getLeastSignificantBits());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public <T extends Message> ChainedDataOutputStream writeArray(T[] array) throws IOException {
 | 
				
			||||||
 | 
							this.out.writeInt(array.length);
 | 
				
			||||||
 | 
							for (var item : array) {
 | 
				
			||||||
 | 
								item.getType().writer().write(item, this);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public <T extends Message> ChainedDataOutputStream writeMessage(Message msg) throws IOException {
 | 
				
			||||||
 | 
							msg.getType().writer().write(msg, this);
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Writes an object to the stream.
 | 
				
			||||||
 | 
						 * @param o The object to write.
 | 
				
			||||||
 | 
						 * @param type The object's type. This is needed in case the object itself
 | 
				
			||||||
 | 
						 *             is null, which may be the case for some strings or ids.
 | 
				
			||||||
 | 
						 * @return The chained output stream.
 | 
				
			||||||
 | 
						 * @throws IOException If an error occurs.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public ChainedDataOutputStream writeObject(Object o, Class<?> type) throws IOException {
 | 
				
			||||||
 | 
							if (type.equals(Integer.class) || type.equals(int.class)) {
 | 
				
			||||||
 | 
								this.writeInt((Integer) o);
 | 
				
			||||||
 | 
							} else if (type.equals(Long.class) || type.equals(long.class)) {
 | 
				
			||||||
 | 
								this.out.writeLong((Long) o);
 | 
				
			||||||
 | 
							} else if (type.equals(String.class)) {
 | 
				
			||||||
 | 
								this.writeString((String) o);
 | 
				
			||||||
 | 
							} else if (type.equals(UUID.class)) {
 | 
				
			||||||
 | 
								this.writeUUID((UUID) o);
 | 
				
			||||||
 | 
							} else if (type.isEnum()) {
 | 
				
			||||||
 | 
								this.writeEnum((Enum<?>) o);
 | 
				
			||||||
 | 
							} else if (type.equals(byte[].class)) {
 | 
				
			||||||
 | 
								byte[] b = (byte[]) o;
 | 
				
			||||||
 | 
								this.writeInt(b.length);
 | 
				
			||||||
 | 
								this.out.write(b);
 | 
				
			||||||
 | 
							} else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
 | 
				
			||||||
 | 
								this.writeArray((Message[]) o);
 | 
				
			||||||
 | 
							} else if (Message.class.isAssignableFrom(type)) {
 | 
				
			||||||
 | 
								this.writeMessage((Message) o);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported object type: " + o.getClass().getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,88 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.MessageType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataInputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.lang.reflect.Array;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * An extended output stream which contains additional methods for reading more
 | 
				
			||||||
 | 
					 * complex types that are used by the Concord system.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ExtendedDataInputStream extends DataInputStream {
 | 
				
			||||||
 | 
						public ExtendedDataInputStream(InputStream in) {
 | 
				
			||||||
 | 
							super(in);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String readString() throws IOException {
 | 
				
			||||||
 | 
							int length = super.readInt();
 | 
				
			||||||
 | 
							if (length == -1) return null;
 | 
				
			||||||
 | 
							if (length == 0) return "";
 | 
				
			||||||
 | 
							byte[] data = new byte[length];
 | 
				
			||||||
 | 
							int read = super.read(data);
 | 
				
			||||||
 | 
							if (read != length) throw new IOException("Not all bytes of a string of length " + length + " could be read.");
 | 
				
			||||||
 | 
							return new String(data, StandardCharsets.UTF_8);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public <T extends Enum<?>> T readEnum(Class<T> e) throws IOException {
 | 
				
			||||||
 | 
							int ordinal = super.readInt();
 | 
				
			||||||
 | 
							if (ordinal == -1) return null;
 | 
				
			||||||
 | 
							return e.getEnumConstants()[ordinal];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public UUID readUUID() throws IOException {
 | 
				
			||||||
 | 
							long a = super.readLong();
 | 
				
			||||||
 | 
							long b = super.readLong();
 | 
				
			||||||
 | 
							if (a == -1 && b == -1) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return new UUID(a, b);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@SuppressWarnings("unchecked")
 | 
				
			||||||
 | 
						public <T extends Message> T[] readArray(MessageType<T> type) throws IOException {
 | 
				
			||||||
 | 
							int length = super.readInt();
 | 
				
			||||||
 | 
							T[] array = (T[]) Array.newInstance(type.messageClass(), length);
 | 
				
			||||||
 | 
							for (int i = 0; i < length; i++) {
 | 
				
			||||||
 | 
								array[i] = type.reader().read(this);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return array;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Reads an object from the stream that is of a certain expected type.
 | 
				
			||||||
 | 
						 * @param type The type of object to read.
 | 
				
			||||||
 | 
						 * @return The object that was read.
 | 
				
			||||||
 | 
						 * @throws IOException If an error occurs while reading.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@SuppressWarnings("unchecked")
 | 
				
			||||||
 | 
						public Object readObject(Class<?> type) throws IOException {
 | 
				
			||||||
 | 
							if (type.equals(Integer.class) || type.equals(int.class)) {
 | 
				
			||||||
 | 
								return this.readInt();
 | 
				
			||||||
 | 
							} else if (type.equals(Long.class) || type.equals(long.class)) {
 | 
				
			||||||
 | 
								return this.readLong();
 | 
				
			||||||
 | 
							} else if (type.equals(String.class)) {
 | 
				
			||||||
 | 
								return this.readString();
 | 
				
			||||||
 | 
							} else if (type.equals(UUID.class)) {
 | 
				
			||||||
 | 
								return this.readUUID();
 | 
				
			||||||
 | 
							} else if (type.isEnum()) {
 | 
				
			||||||
 | 
								return this.readEnum((Class<? extends Enum<?>>) type);
 | 
				
			||||||
 | 
							} else if (type.isAssignableFrom(byte[].class)) {
 | 
				
			||||||
 | 
								int length = this.readInt();
 | 
				
			||||||
 | 
								return this.readNBytes(length);
 | 
				
			||||||
 | 
							} else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
 | 
				
			||||||
 | 
								var messageType = MessageType.get((Class<? extends Message>) type.getComponentType());
 | 
				
			||||||
 | 
								return this.readArray(messageType);
 | 
				
			||||||
 | 
							} else if (Message.class.isAssignableFrom(type)) {
 | 
				
			||||||
 | 
								var messageType = MessageType.get((Class<? extends Message>) type);
 | 
				
			||||||
 | 
								return messageType.reader().read(this);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported object type: " + type.getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record Triple<A, B, C> (A first, B second, C third) {}
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,6 @@ import java.util.concurrent.ExecutorService;
 | 
				
			||||||
import java.util.concurrent.Executors;
 | 
					import java.util.concurrent.Executors;
 | 
				
			||||||
import java.util.concurrent.ScheduledExecutorService;
 | 
					import java.util.concurrent.ScheduledExecutorService;
 | 
				
			||||||
import java.util.concurrent.TimeUnit;
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The main server implementation, which handles accepting new clients.
 | 
					 * The main server implementation, which handles accepting new clients.
 | 
				
			||||||
| 
						 | 
					@ -128,8 +127,8 @@ public class ConcordServer implements Runnable {
 | 
				
			||||||
				this.config.getName(),
 | 
									this.config.getName(),
 | 
				
			||||||
				this.channelManager.getChannels().stream()
 | 
									this.channelManager.getChannels().stream()
 | 
				
			||||||
						.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
 | 
											.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
 | 
				
			||||||
						.sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
 | 
											.sorted(Comparator.comparing(ServerMetaData.ChannelData::name))
 | 
				
			||||||
						.collect(Collectors.toList())
 | 
											.toList().toArray(new ServerMetaData.ChannelData[0])
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,7 +76,7 @@ public class Channel implements Comparable<Channel> {
 | 
				
			||||||
	 * @throws IOException If an error occurs.
 | 
						 * @throws IOException If an error occurs.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void sendMessage(Message msg) throws IOException {
 | 
						public void sendMessage(Message msg) throws IOException {
 | 
				
			||||||
		ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1);
 | 
							ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.byteSize() + 1);
 | 
				
			||||||
		this.server.getSerializer().writeMessage(msg, baos);
 | 
							this.server.getSerializer().writeMessage(msg, baos);
 | 
				
			||||||
		byte[] data = baos.toByteArray();
 | 
							byte[] data = baos.toByteArray();
 | 
				
			||||||
		for (var client : this.connectedClients) {
 | 
							for (var client : this.connectedClients) {
 | 
				
			||||||
| 
						 | 
					@ -93,7 +93,7 @@ public class Channel implements Comparable<Channel> {
 | 
				
			||||||
		for (var clientThread : this.getConnectedClients()) {
 | 
							for (var clientThread : this.getConnectedClients()) {
 | 
				
			||||||
			users.add(clientThread.toData());
 | 
								users.add(clientThread.toData());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		users.sort(Comparator.comparing(UserData::getName));
 | 
							users.sort(Comparator.comparing(UserData::name));
 | 
				
			||||||
		return users;
 | 
							return users;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
package nl.andrewl.concord_server.channel;
 | 
					package nl.andrewl.concord_server.channel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.client.ClientThread;
 | 
					import nl.andrewl.concord_server.client.ClientThread;
 | 
				
			||||||
import nl.andrewl.concord_server.util.CollectionUtils;
 | 
					import nl.andrewl.concord_server.util.CollectionUtils;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ public class ListClientsCommand implements ServerCliCommand {
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			StringBuilder sb = new StringBuilder("Online Users:\n");
 | 
								StringBuilder sb = new StringBuilder("Online Users:\n");
 | 
				
			||||||
			for (var userData : users) {
 | 
								for (var userData : users) {
 | 
				
			||||||
				sb.append("\t").append(userData.getName()).append(" (").append(userData.getId()).append(")\n");
 | 
									sb.append("\t").append(userData.name()).append(" (").append(userData.id()).append(")\n");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			System.out.print(sb);
 | 
								System.out.print(sb);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,10 @@ package nl.andrewl.concord_server.client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Error;
 | 
					import nl.andrewl.concord_core.msg.types.Error;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.*;
 | 
					import nl.andrewl.concord_core.msg.types.ServerUsers;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.UserData;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.util.CollectionUtils;
 | 
					import nl.andrewl.concord_server.util.CollectionUtils;
 | 
				
			||||||
import nl.andrewl.concord_server.util.StringUtils;
 | 
					import nl.andrewl.concord_server.util.StringUtils;
 | 
				
			||||||
| 
						 | 
					@ -54,7 +57,7 @@ public class ClientManager {
 | 
				
			||||||
	public void handleLogIn(Identification identification, ClientThread clientThread) {
 | 
						public void handleLogIn(Identification identification, ClientThread clientThread) {
 | 
				
			||||||
		ClientConnectionData data;
 | 
							ClientConnectionData data;
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			data = identification.getSessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
 | 
								data = identification.sessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
 | 
				
			||||||
		} catch (InvalidIdentificationException e) {
 | 
							} catch (InvalidIdentificationException e) {
 | 
				
			||||||
			clientThread.sendToClient(Error.warning(e.getMessage()));
 | 
								clientThread.sendToClient(Error.warning(e.getMessage()));
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
| 
						 | 
					@ -75,7 +78,7 @@ public class ClientManager {
 | 
				
			||||||
				data.newClient ? " for the first time" : "",
 | 
									data.newClient ? " for the first time" : "",
 | 
				
			||||||
				defaultChannel
 | 
									defaultChannel
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		this.broadcast(new ServerUsers(this.getClients()));
 | 
							this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -89,7 +92,7 @@ public class ClientManager {
 | 
				
			||||||
			client.getCurrentChannel().removeClient(client);
 | 
								client.getCurrentChannel().removeClient(client);
 | 
				
			||||||
			client.shutdown();
 | 
								client.shutdown();
 | 
				
			||||||
			System.out.println("Client " + client + " has disconnected.");
 | 
								System.out.println("Client " + client + " has disconnected.");
 | 
				
			||||||
			this.broadcast(new ServerUsers(this.getClients()));
 | 
								this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,7 +102,7 @@ public class ClientManager {
 | 
				
			||||||
	 * @param message The message to send.
 | 
						 * @param message The message to send.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void broadcast(Message message) {
 | 
						public void broadcast(Message message) {
 | 
				
			||||||
		ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
 | 
							ByteArrayOutputStream baos = new ByteArrayOutputStream(message.byteSize());
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			this.server.getSerializer().writeMessage(message, baos);
 | 
								this.server.getSerializer().writeMessage(message, baos);
 | 
				
			||||||
			byte[] data = baos.toByteArray();
 | 
								byte[] data = baos.toByteArray();
 | 
				
			||||||
| 
						 | 
					@ -129,11 +132,11 @@ public class ClientManager {
 | 
				
			||||||
	private static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
 | 
						private static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private ClientConnectionData getClientDataFromDb(Identification identification) throws InvalidIdentificationException {
 | 
						private ClientConnectionData getClientDataFromDb(Identification identification) throws InvalidIdentificationException {
 | 
				
			||||||
		var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.getSessionToken()));
 | 
							var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.sessionToken()));
 | 
				
			||||||
		Document doc = cursor.firstOrDefault();
 | 
							Document doc = cursor.firstOrDefault();
 | 
				
			||||||
		if (doc != null) {
 | 
							if (doc != null) {
 | 
				
			||||||
			UUID id = doc.get("id", UUID.class);
 | 
								UUID id = doc.get("id", UUID.class);
 | 
				
			||||||
			String nickname = identification.getNickname();
 | 
								String nickname = identification.nickname();
 | 
				
			||||||
			if (nickname != null) {
 | 
								if (nickname != null) {
 | 
				
			||||||
				doc.put("nickname", nickname);
 | 
									doc.put("nickname", nickname);
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					@ -150,7 +153,7 @@ public class ClientManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private ClientConnectionData getNewClientData(Identification identification) throws InvalidIdentificationException {
 | 
						private ClientConnectionData getNewClientData(Identification identification) throws InvalidIdentificationException {
 | 
				
			||||||
		UUID id = this.server.getIdProvider().newId();
 | 
							UUID id = this.server.getIdProvider().newId();
 | 
				
			||||||
		String nickname = identification.getNickname();
 | 
							String nickname = identification.nickname();
 | 
				
			||||||
		if (nickname == null) {
 | 
							if (nickname == null) {
 | 
				
			||||||
			throw new InvalidIdentificationException("Missing nickname.");
 | 
								throw new InvalidIdentificationException("Missing nickname.");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import lombok.Getter;
 | 
				
			||||||
import lombok.Setter;
 | 
					import lombok.Setter;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Encryption;
 | 
					import nl.andrewl.concord_core.msg.Encryption;
 | 
				
			||||||
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.client_setup.Identification;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.UserData;
 | 
					import nl.andrewl.concord_core.msg.types.UserData;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.channel.Channel;
 | 
					import nl.andrewl.concord_server.channel.Channel;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,10 @@
 | 
				
			||||||
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.Error;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.client.ClientThread;
 | 
					import nl.andrewl.concord_server.client.ClientThread;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -17,11 +16,11 @@ import java.util.Set;
 | 
				
			||||||
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.getId());
 | 
							var optionalChannel = server.getChannelManager().getChannelById(msg.id());
 | 
				
			||||||
		if (optionalChannel.isPresent()) {
 | 
							if (optionalChannel.isPresent()) {
 | 
				
			||||||
			server.getChannelManager().moveToChannel(client, optionalChannel.get());
 | 
								server.getChannelManager().moveToChannel(client, optionalChannel.get());
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			var optionalClient = server.getClientManager().getClientById(msg.getId());
 | 
								var optionalClient = server.getClientManager().getClientById(msg.id());
 | 
				
			||||||
			if (optionalClient.isPresent()) {
 | 
								if (optionalClient.isPresent()) {
 | 
				
			||||||
				var privateChannel = server.getChannelManager().getPrivateChannel(Set.of(
 | 
									var privateChannel = server.getChannelManager().getPrivateChannel(Set.of(
 | 
				
			||||||
						client.getClientId(),
 | 
											client.getClientId(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.Error;
 | 
					import nl.andrewl.concord_core.msg.types.Error;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.client.ClientThread;
 | 
					import nl.andrewl.concord_server.client.ClientThread;
 | 
				
			||||||
import org.dizitart.no2.Document;
 | 
					import org.dizitart.no2.Document;
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import java.util.Map;
 | 
				
			||||||
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 {
 | 
				
			||||||
		if (msg.getMessage().length() > server.getConfig().getMaxMessageLength()) {
 | 
							if (msg.message().length() > server.getConfig().getMaxMessageLength()) {
 | 
				
			||||||
			client.getCurrentChannel().sendMessage(Error.warning("Message is too long."));
 | 
								client.getCurrentChannel().sendMessage(Error.warning("Message is too long."));
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -27,17 +27,17 @@ public class ChatHandler implements MessageHandler<Chat> {
 | 
				
			||||||
		malicious UUID, so we overwrite it with a server-generated id which we
 | 
							malicious UUID, so we overwrite it with a server-generated id which we
 | 
				
			||||||
		know is safe.
 | 
							know is safe.
 | 
				
			||||||
		 */
 | 
							 */
 | 
				
			||||||
		msg.setId(server.getIdProvider().newId());
 | 
							msg = new Chat(server.getIdProvider().newId(), msg);
 | 
				
			||||||
		var collection = client.getCurrentChannel().getMessageCollection();
 | 
							var collection = client.getCurrentChannel().getMessageCollection();
 | 
				
			||||||
		Document doc = new Document(Map.of(
 | 
							Document doc = new Document(Map.of(
 | 
				
			||||||
				"id", msg.getId(),
 | 
									"id", msg.id(),
 | 
				
			||||||
				"senderId", msg.getSenderId(),
 | 
									"senderId", msg.senderId(),
 | 
				
			||||||
				"senderNickname", msg.getSenderNickname(),
 | 
									"senderNickname", msg.senderNickname(),
 | 
				
			||||||
				"timestamp", msg.getTimestamp(),
 | 
									"timestamp", msg.timestamp(),
 | 
				
			||||||
				"message", msg.getMessage()
 | 
									"message", msg.message()
 | 
				
			||||||
		));
 | 
							));
 | 
				
			||||||
		collection.insert(doc);
 | 
							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.message());
 | 
				
			||||||
		client.getCurrentChannel().sendMessage(msg);
 | 
							client.getCurrentChannel().sendMessage(msg);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
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.ChatHistoryRequest;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
 | 
					 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.Error;
 | 
					import nl.andrewl.concord_core.msg.types.Error;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.channel.Channel;
 | 
					import nl.andrewl.concord_server.channel.Channel;
 | 
				
			||||||
import nl.andrewl.concord_server.client.ClientThread;
 | 
					import nl.andrewl.concord_server.client.ClientThread;
 | 
				
			||||||
| 
						 | 
					@ -19,10 +19,10 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
 | 
						public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
 | 
				
			||||||
		// First try and find a public channel with the given id.
 | 
							// First try and find a public channel with the given id.
 | 
				
			||||||
		var channel = server.getChannelManager().getChannelById(msg.getChannelId()).orElse(null);
 | 
							var channel = server.getChannelManager().getChannelById(msg.channelId()).orElse(null);
 | 
				
			||||||
		if (channel == null) {
 | 
							if (channel == null) {
 | 
				
			||||||
			// Couldn't find a public channel, so look for a private channel this client is involved in.
 | 
								// 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);
 | 
								channel = server.getChannelManager().getPrivateChannel(client.getClientId(), msg.channelId()).orElse(null);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// If we couldn't find a public or private channel, give up.
 | 
							// If we couldn't find a public or private channel, give up.
 | 
				
			||||||
		if (channel == null) {
 | 
							if (channel == null) {
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
 | 
				
			||||||
		for (var doc : cursor) {
 | 
							for (var doc : cursor) {
 | 
				
			||||||
			chats.add(this.read(doc));
 | 
								chats.add(this.read(doc));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		client.sendToClient(new ChatHistoryResponse(channel.getId(), chats));
 | 
							client.sendToClient(new ChatHistoryResponse(channel.getId(), chats.toArray(new Chat[0])));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -89,7 +89,7 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
 | 
				
			||||||
			chats.add(this.read(doc));
 | 
								chats.add(this.read(doc));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		Collections.reverse(chats);
 | 
							Collections.reverse(chats);
 | 
				
			||||||
		return new ChatHistoryResponse(channel.getId(), chats);
 | 
							return new ChatHistoryResponse(channel.getId(), chats.toArray(new Chat[0]));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,9 +2,9 @@ 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.Chat;
 | 
					import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
 | 
					import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
 | 
					import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_server.ConcordServer;
 | 
					import nl.andrewl.concord_server.ConcordServer;
 | 
				
			||||||
import nl.andrewl.concord_server.client.ClientThread;
 | 
					import nl.andrewl.concord_server.client.ClientThread;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue