Added initial prototype.
This commit is contained in:
		
							parent
							
								
									ae50a15c00
								
							
						
					
					
						commit
						468758c7ab
					
				| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					/.idea
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
							
								
								
									
										11
									
								
								README.md
								
								
								
								
							
							
						
						
									
										11
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -1,2 +1,11 @@
 | 
				
			||||||
# Concord
 | 
					# Concord
 | 
				
			||||||
Console-based real-time messaging platform.
 | 
					Console-based real-time messaging platform, inspired by Discord.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This platform is organized by many independent servers, each of which supports the following:
 | 
				
			||||||
 | 
					- Multiple message channels. By default, there's one `general` channel.
 | 
				
			||||||
 | 
					- Broadcasting itself on certain discovery servers for users to find. The server decides where it wants to be discovered, if at all.
 | 
				
			||||||
 | 
					- Starting threads as spin-offs of a source message (with infinite recursion, i.e. threads within threads).
 | 
				
			||||||
 | 
					- Private message between users in a server. **No support for private messaging users outside the context of a server.**
 | 
				
			||||||
 | 
					- Banning users from the server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each server uses a single [Nitrite](https://www.dizitart.org/nitrite-database/#what-is-nitrite) database to hold messages and other information.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
				
			||||||
 | 
					         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
					    <parent>
 | 
				
			||||||
 | 
					        <artifactId>concord</artifactId>
 | 
				
			||||||
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					        <version>1.0-SNAPSHOT</version>
 | 
				
			||||||
 | 
					    </parent>
 | 
				
			||||||
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <artifactId>concord-client</artifactId>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependencies>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					            <artifactId>concord-core</artifactId>
 | 
				
			||||||
 | 
					            <version>${project.parent.version}</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.googlecode.lanterna</groupId>
 | 
				
			||||||
 | 
					            <artifactId>lanterna</artifactId>
 | 
				
			||||||
 | 
					            <version>3.1.1</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					module concord_client {
 | 
				
			||||||
 | 
						requires concord_core;
 | 
				
			||||||
 | 
						requires com.googlecode.lanterna;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface ClientMessageListener {
 | 
				
			||||||
 | 
						void messageReceived(ConcordClient client, Message message) throws IOException;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,109 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.Window;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.screen.Screen;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.screen.TerminalScreen;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.terminal.Terminal;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_client.gui.MainWindow;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Serializer;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.ServerWelcome;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataInputStream;
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.net.Socket;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ConcordClient implements Runnable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final Socket socket;
 | 
				
			||||||
 | 
						private final DataInputStream in;
 | 
				
			||||||
 | 
						private final DataOutputStream out;
 | 
				
			||||||
 | 
						private final long id;
 | 
				
			||||||
 | 
						private final String nickname;
 | 
				
			||||||
 | 
						private final Set<ClientMessageListener> messageListeners;
 | 
				
			||||||
 | 
						private volatile boolean running;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ConcordClient(String host, int port, String nickname) throws IOException {
 | 
				
			||||||
 | 
							this.socket = new Socket(host, port);
 | 
				
			||||||
 | 
							this.in = new DataInputStream(this.socket.getInputStream());
 | 
				
			||||||
 | 
							this.out = new DataOutputStream(this.socket.getOutputStream());
 | 
				
			||||||
 | 
							this.nickname = nickname;
 | 
				
			||||||
 | 
							Serializer.writeMessage(new Identification(nickname), this.out);
 | 
				
			||||||
 | 
							Message reply = Serializer.readMessage(this.in);
 | 
				
			||||||
 | 
							if (reply instanceof ServerWelcome welcome) {
 | 
				
			||||||
 | 
								this.id = welcome.getClientId();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IOException("Unexpected response from the server after sending identification message.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.messageListeners = new HashSet<>();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void addListener(ClientMessageListener listener) {
 | 
				
			||||||
 | 
							this.messageListeners.add(listener);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void removeListener(ClientMessageListener listener) {
 | 
				
			||||||
 | 
							this.messageListeners.remove(listener);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void sendChat(String message) throws IOException {
 | 
				
			||||||
 | 
							Serializer.writeMessage(new Chat(this.id, this.nickname, System.currentTimeMillis(), message), this.out);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void shutdown() {
 | 
				
			||||||
 | 
							this.running = false;
 | 
				
			||||||
 | 
							if (!this.socket.isClosed()) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									this.socket.close();
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									e.printStackTrace();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void run() {
 | 
				
			||||||
 | 
							this.running = true;
 | 
				
			||||||
 | 
							while (this.running) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									Message msg = Serializer.readMessage(this.in);
 | 
				
			||||||
 | 
									for (var listener : this.messageListeners) {
 | 
				
			||||||
 | 
										listener.messageReceived(this, msg);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									e.printStackTrace();
 | 
				
			||||||
 | 
									this.running = false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								this.socket.close();
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void main(String[] args) throws IOException {
 | 
				
			||||||
 | 
							Terminal term = new DefaultTerminalFactory().createTerminal();
 | 
				
			||||||
 | 
							Screen screen = new TerminalScreen(term);
 | 
				
			||||||
 | 
							WindowBasedTextGUI gui = new MultiWindowTextGUI(screen);
 | 
				
			||||||
 | 
							screen.startScreen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Window window = new MainWindow();
 | 
				
			||||||
 | 
							window.setHints(List.of(Window.Hint.FULL_SCREEN));
 | 
				
			||||||
 | 
							gui.addWindow(window);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							window.waitUntilClosed();
 | 
				
			||||||
 | 
							screen.stopScreen();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ChannelList extends AbstractListBox<String, ChannelList> {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ChatList extends AbstractListBox<Chat, ChatList> {
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Adds one more item to the list box, at the end.
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @param item Item to add to the list box
 | 
				
			||||||
 | 
						 * @return Itself
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public synchronized ChatList addItem(Chat item) {
 | 
				
			||||||
 | 
							super.addItem(item);
 | 
				
			||||||
 | 
							this.setSelectedIndex(this.getItemCount() - 1);
 | 
				
			||||||
 | 
							return this;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the
 | 
				
			||||||
 | 
						 * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the
 | 
				
			||||||
 | 
						 * entire list box but for each item, called one by one.
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @return {@code ListItemRenderer} to use when drawing the items in the list
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						protected ListItemRenderer<Chat, ChatList> createDefaultListItemRenderer() {
 | 
				
			||||||
 | 
							return new ChatRenderer();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,91 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.TerminalSize;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.*;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.input.KeyStroke;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.input.KeyType;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_client.ClientMessageListener;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_client.ConcordClient;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicBoolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The main panel in which a user interacts with the application during normal
 | 
				
			||||||
 | 
					 * operation. In here, the user is shown a list of the most recent chats in
 | 
				
			||||||
 | 
					 * their current channel or thread, a text-box for sending a message, and some
 | 
				
			||||||
 | 
					 * meta information in the sidebars which provides the user with a list of all
 | 
				
			||||||
 | 
					 * threads and users in the server.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ChatPanel extends Panel implements ClientMessageListener {
 | 
				
			||||||
 | 
						private final ChatList chatList;
 | 
				
			||||||
 | 
						private final TextBox inputTextBox;
 | 
				
			||||||
 | 
						private final ChannelList channelList;
 | 
				
			||||||
 | 
						private final UserList userList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final ConcordClient client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ChatPanel(ConcordClient client, Window window) {
 | 
				
			||||||
 | 
							super(new BorderLayout());
 | 
				
			||||||
 | 
							this.client = client;
 | 
				
			||||||
 | 
							this.chatList = new ChatList();
 | 
				
			||||||
 | 
							this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
 | 
				
			||||||
 | 
							this.inputTextBox.setCaretWarp(true);
 | 
				
			||||||
 | 
							this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.channelList = new ChannelList();
 | 
				
			||||||
 | 
							this.channelList.addItem("general");
 | 
				
			||||||
 | 
							this.channelList.addItem("memes");
 | 
				
			||||||
 | 
							this.channelList.addItem("testing");
 | 
				
			||||||
 | 
							this.userList = new UserList();
 | 
				
			||||||
 | 
							this.userList.addItem("andrew");
 | 
				
			||||||
 | 
							this.userList.addItem("tester");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							window.addWindowListener(new WindowListenerAdapter() {
 | 
				
			||||||
 | 
								@Override
 | 
				
			||||||
 | 
								public void onInput(Window basePane, KeyStroke keyStroke, AtomicBoolean deliverEvent) {
 | 
				
			||||||
 | 
									if (keyStroke.getKeyType() == KeyType.Enter) {
 | 
				
			||||||
 | 
										if (keyStroke.isShiftDown()) {
 | 
				
			||||||
 | 
											System.out.println("Adding newline");
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											String text = inputTextBox.getText();
 | 
				
			||||||
 | 
											if (text != null && !text.isBlank()) {
 | 
				
			||||||
 | 
												try {
 | 
				
			||||||
 | 
													System.out.println("Sending: " + text.trim());
 | 
				
			||||||
 | 
													client.sendChat(text.trim());
 | 
				
			||||||
 | 
													inputTextBox.setText("");
 | 
				
			||||||
 | 
												} catch (IOException e) {
 | 
				
			||||||
 | 
													e.printStackTrace();
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											deliverEvent.set(false);
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							Border b;
 | 
				
			||||||
 | 
							b = Borders.doubleLine("Channels");
 | 
				
			||||||
 | 
							b.setComponent(this.channelList);
 | 
				
			||||||
 | 
							this.addComponent(b, BorderLayout.Location.LEFT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b = Borders.doubleLine("Users");
 | 
				
			||||||
 | 
							b.setComponent(this.userList);
 | 
				
			||||||
 | 
							this.addComponent(b, BorderLayout.Location.RIGHT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b = Borders.doubleLine("#general");
 | 
				
			||||||
 | 
							b.setComponent(this.chatList);
 | 
				
			||||||
 | 
							this.addComponent(b, BorderLayout.Location.CENTER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.addComponent(this.inputTextBox, BorderLayout.Location.BOTTOM);
 | 
				
			||||||
 | 
							this.inputTextBox.takeFocus();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void messageReceived(ConcordClient client, Message message) throws IOException {
 | 
				
			||||||
 | 
							if (message instanceof Chat chat) {
 | 
				
			||||||
 | 
								this.chatList.addItem(chat);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.TerminalTextUtils;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.graphics.ThemeDefinition;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.TextGUIGraphics;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Instant;
 | 
				
			||||||
 | 
					import java.time.ZoneId;
 | 
				
			||||||
 | 
					import java.time.format.DateTimeFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ChatRenderer extends AbstractListBox.ListItemRenderer<Chat, ChatList> {
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void drawItem(TextGUIGraphics graphics, ChatList listBox, int index, Chat chat, boolean selected, boolean focused) {
 | 
				
			||||||
 | 
							ThemeDefinition themeDefinition = listBox.getTheme().getDefinition(AbstractListBox.class);
 | 
				
			||||||
 | 
							if(selected && focused) {
 | 
				
			||||||
 | 
								graphics.applyThemeStyle(themeDefinition.getSelected());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								graphics.applyThemeStyle(themeDefinition.getNormal());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							graphics.putString(0, 0, chat.getSenderNickname());
 | 
				
			||||||
 | 
							Instant timestamp = Instant.ofEpochMilli(chat.getTimestamp());
 | 
				
			||||||
 | 
							String timeStr = timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm:ss"));
 | 
				
			||||||
 | 
							String label = chat.getSenderNickname() + " @ " + timeStr + " : " + chat.getMessage();
 | 
				
			||||||
 | 
							label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
 | 
				
			||||||
 | 
							while(TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) {
 | 
				
			||||||
 | 
								label += " ";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							graphics.putString(0, 0, label);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.*;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.input.KeyStroke;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_client.ConcordClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicBoolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class MainWindow extends BasicWindow {
 | 
				
			||||||
 | 
						public MainWindow() {
 | 
				
			||||||
 | 
							super("Concord");
 | 
				
			||||||
 | 
							Panel panel = new Panel(new LinearLayout(Direction.VERTICAL));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Button button = new Button("Connect to Server");
 | 
				
			||||||
 | 
							button.addListener(b -> connectToServer());
 | 
				
			||||||
 | 
							panel.addComponent(button);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.setComponent(panel);
 | 
				
			||||||
 | 
							this.addWindowListener(new WindowListenerAdapter() {
 | 
				
			||||||
 | 
								@Override
 | 
				
			||||||
 | 
								public void onInput(Window basePane, KeyStroke keyStroke, AtomicBoolean hasBeenHandled) {
 | 
				
			||||||
 | 
									if (keyStroke.getCharacter() != null && keyStroke.getCharacter() == 'c' && keyStroke.isCtrlDown()) {
 | 
				
			||||||
 | 
										System.exit(0);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void connectToServer() {
 | 
				
			||||||
 | 
							System.out.println("Connecting to server!");
 | 
				
			||||||
 | 
							var addressDialog = new TextInputDialogBuilder()
 | 
				
			||||||
 | 
									.setTitle("Server Address")
 | 
				
			||||||
 | 
									.setDescription("Enter the address of the server to connect to. For example, \"localhost:1234\".")
 | 
				
			||||||
 | 
									.setInitialContent("localhost:8123")
 | 
				
			||||||
 | 
									.build();
 | 
				
			||||||
 | 
							String address = addressDialog.showDialog(this.getTextGUI());
 | 
				
			||||||
 | 
							if (address == null) return;
 | 
				
			||||||
 | 
							String[] parts = address.split(":");
 | 
				
			||||||
 | 
							if (parts.length != 2) return;
 | 
				
			||||||
 | 
							String host = parts[0];
 | 
				
			||||||
 | 
							int port = Integer.parseInt(parts[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var nameDialog = new TextInputDialogBuilder()
 | 
				
			||||||
 | 
									.setTitle("Nickname")
 | 
				
			||||||
 | 
									.setDescription("Enter a nickname to use while in the server.")
 | 
				
			||||||
 | 
									.build();
 | 
				
			||||||
 | 
							String nickname = nameDialog.showDialog(this.getTextGUI());
 | 
				
			||||||
 | 
							if (nickname == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								var client = new ConcordClient(host, port, nickname);
 | 
				
			||||||
 | 
								var chatPanel = new ChatPanel(client, this);
 | 
				
			||||||
 | 
								client.addListener(chatPanel);
 | 
				
			||||||
 | 
								new Thread(client).start();
 | 
				
			||||||
 | 
								this.setComponent(chatPanel);
 | 
				
			||||||
 | 
								Borders.joinLinesWithFrame(this.getTextGUI().getScreen().newTextGraphics());
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_client.gui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class UserList extends AbstractListBox<String, UserList> {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
				
			||||||
 | 
					         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
					    <parent>
 | 
				
			||||||
 | 
					        <artifactId>concord</artifactId>
 | 
				
			||||||
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					        <version>1.0-SNAPSHOT</version>
 | 
				
			||||||
 | 
					    </parent>
 | 
				
			||||||
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <artifactId>concord-core</artifactId>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					module concord_core {
 | 
				
			||||||
 | 
						requires static lombok;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						exports nl.andrewl.concord_core.msg to concord_server, concord_client;
 | 
				
			||||||
 | 
						exports nl.andrewl.concord_core.msg.types to concord_server, concord_client;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,85 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataInputStream;
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents any message which can be sent over the network.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     All messages consist of a single byte type identifier, followed by a
 | 
				
			||||||
 | 
					 *     payload whose structure depends on the message.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public interface Message {
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return The exact number of bytes that this message will use when written
 | 
				
			||||||
 | 
						 * to a stream.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						int getByteCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Utility methods.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Gets the number of bytes that the given string will occupy when it is
 | 
				
			||||||
 | 
						 * serialized.
 | 
				
			||||||
 | 
						 * @param s The string.
 | 
				
			||||||
 | 
						 * @return The number of bytes used to serialize the string.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						default int getByteSize(String s) {
 | 
				
			||||||
 | 
							return Integer.BYTES + s.getBytes(StandardCharsets.UTF_8).length;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Writes a string to the given output stream using a length-prefixed format
 | 
				
			||||||
 | 
						 * where an integer length precedes the string's bytes, which are encoded in
 | 
				
			||||||
 | 
						 * UTF-8.
 | 
				
			||||||
 | 
						 * @param s The string to write.
 | 
				
			||||||
 | 
						 * @param o The output stream to write to.
 | 
				
			||||||
 | 
						 * @throws IOException If the stream could not be written to.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						default void writeString(String s, DataOutputStream o) throws IOException {
 | 
				
			||||||
 | 
							if (s == null) {
 | 
				
			||||||
 | 
								o.writeInt(-1);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								o.writeInt(s.length());
 | 
				
			||||||
 | 
								o.write(s.getBytes(StandardCharsets.UTF_8));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						default String readString(DataInputStream i) throws IOException {
 | 
				
			||||||
 | 
							int length = i.readInt();
 | 
				
			||||||
 | 
							if (length == -1) return null;
 | 
				
			||||||
 | 
							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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.ServerWelcome;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.*;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This class is responsible for reading and writing messages from streams.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class Serializer {
 | 
				
			||||||
 | 
						private static final Map<Byte, Class<? extends Message>> messageTypes = new HashMap<>();
 | 
				
			||||||
 | 
						private static final Map<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>();
 | 
				
			||||||
 | 
						static {
 | 
				
			||||||
 | 
							registerType(0, Identification.class);
 | 
				
			||||||
 | 
							registerType(1, ServerWelcome.class);
 | 
				
			||||||
 | 
							registerType(2, Chat.class);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static void registerType(int id, Class<? extends Message> clazz) {
 | 
				
			||||||
 | 
							messageTypes.put((byte) id, clazz);
 | 
				
			||||||
 | 
							inverseMessageTypes.put(clazz, (byte) id);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static Message readMessage(InputStream i) throws IOException {
 | 
				
			||||||
 | 
							DataInputStream d = new DataInputStream(i);
 | 
				
			||||||
 | 
							byte type = d.readByte();
 | 
				
			||||||
 | 
							var clazz = messageTypes.get(type);
 | 
				
			||||||
 | 
							if (clazz == null) {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported message type: " + type);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								var constructor = clazz.getConstructor();
 | 
				
			||||||
 | 
								var message = constructor.newInstance();
 | 
				
			||||||
 | 
								message.read(d);
 | 
				
			||||||
 | 
								return message;
 | 
				
			||||||
 | 
							} catch (Throwable e) {
 | 
				
			||||||
 | 
								throw new IOException("Could not instantiate new message object of type " + clazz.getSimpleName(), e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void writeMessage(Message msg, OutputStream o) throws IOException {
 | 
				
			||||||
 | 
							DataOutputStream d = new DataOutputStream(o);
 | 
				
			||||||
 | 
							Byte type = inverseMessageTypes.get(msg.getClass());
 | 
				
			||||||
 | 
							if (type == null) {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							d.writeByte(type);
 | 
				
			||||||
 | 
							msg.write(d);
 | 
				
			||||||
 | 
							d.flush();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This message contains information about a chat message that a user sent.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Data
 | 
				
			||||||
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					public class Chat implements Message {
 | 
				
			||||||
 | 
						private long senderId;
 | 
				
			||||||
 | 
						private String senderNickname;
 | 
				
			||||||
 | 
						private long timestamp;
 | 
				
			||||||
 | 
						private String message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Chat(long senderId, String senderNickname, long timestamp, String message) {
 | 
				
			||||||
 | 
							this.senderId = senderId;
 | 
				
			||||||
 | 
							this.senderNickname = senderNickname;
 | 
				
			||||||
 | 
							this.timestamp = timestamp;
 | 
				
			||||||
 | 
							this.message = message;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Chat(String message) {
 | 
				
			||||||
 | 
							this(-1, null, System.currentTimeMillis(), message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public int getByteCount() {
 | 
				
			||||||
 | 
							return 2 * Long.BYTES + getByteSize(this.message) + getByteSize(this.senderNickname);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void write(DataOutputStream o) throws IOException {
 | 
				
			||||||
 | 
							o.writeLong(this.senderId);
 | 
				
			||||||
 | 
							writeString(this.senderNickname, o);
 | 
				
			||||||
 | 
							o.writeLong(this.timestamp);
 | 
				
			||||||
 | 
							writeString(this.message, o);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void read(DataInputStream i) throws IOException {
 | 
				
			||||||
 | 
							this.senderId = i.readLong();
 | 
				
			||||||
 | 
							this.senderNickname = readString(i);
 | 
				
			||||||
 | 
							this.timestamp = i.readLong();
 | 
				
			||||||
 | 
							this.message = readString(i);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public String toString() {
 | 
				
			||||||
 | 
							return String.format("%s(%d): %s", this.senderNickname, this.senderId, this.message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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
 | 
				
			||||||
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					public class Identification implements Message {
 | 
				
			||||||
 | 
						private String nickname;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Identification(String nickname) {
 | 
				
			||||||
 | 
							this.nickname = nickname;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public int getByteCount() {
 | 
				
			||||||
 | 
							return getByteSize(this.nickname);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void write(DataOutputStream o) throws IOException {
 | 
				
			||||||
 | 
							writeString(this.nickname, o);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void read(DataInputStream i) throws IOException {
 | 
				
			||||||
 | 
							this.nickname = readString(i);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.Data;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataInputStream;
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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
 | 
				
			||||||
 | 
					public class ServerWelcome implements Message {
 | 
				
			||||||
 | 
						private long clientId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public int getByteCount() {
 | 
				
			||||||
 | 
							return Long.BYTES;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void write(DataOutputStream o) throws IOException {
 | 
				
			||||||
 | 
							o.writeLong(this.clientId);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void read(DataInputStream i) throws IOException {
 | 
				
			||||||
 | 
							this.clientId = i.readLong();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This package contains all the components needed by both the server and the
 | 
				
			||||||
 | 
					 * client.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
				
			||||||
 | 
					         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					    <artifactId>concord</artifactId>
 | 
				
			||||||
 | 
					    <packaging>pom</packaging>
 | 
				
			||||||
 | 
					    <version>1.0-SNAPSHOT</version>
 | 
				
			||||||
 | 
					    <modules>
 | 
				
			||||||
 | 
					        <module>server</module>
 | 
				
			||||||
 | 
					        <module>core</module>
 | 
				
			||||||
 | 
					        <module>client</module>
 | 
				
			||||||
 | 
					    </modules>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <properties>
 | 
				
			||||||
 | 
					        <maven.compiler.source>16</maven.compiler.source>
 | 
				
			||||||
 | 
					        <maven.compiler.target>16</maven.compiler.target>
 | 
				
			||||||
 | 
					        <java.version>16</java.version>
 | 
				
			||||||
 | 
					        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
				
			||||||
 | 
					    </properties>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependencies>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.projectlombok</groupId>
 | 
				
			||||||
 | 
					            <artifactId>lombok</artifactId>
 | 
				
			||||||
 | 
					            <version>1.18.20</version>
 | 
				
			||||||
 | 
					            <scope>provided</scope>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <build>
 | 
				
			||||||
 | 
					        <plugins>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-compiler-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>3.8.1</version>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <annotationProcessorPaths>
 | 
				
			||||||
 | 
					                        <path>
 | 
				
			||||||
 | 
					                            <groupId>org.projectlombok</groupId>
 | 
				
			||||||
 | 
					                            <artifactId>lombok</artifactId>
 | 
				
			||||||
 | 
					                            <version>1.18.20</version>
 | 
				
			||||||
 | 
					                        </path>
 | 
				
			||||||
 | 
					                    </annotationProcessorPaths>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
 | 
					        </plugins>
 | 
				
			||||||
 | 
					    </build>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
				
			||||||
 | 
					         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
					    <parent>
 | 
				
			||||||
 | 
					        <artifactId>concord</artifactId>
 | 
				
			||||||
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					        <version>1.0-SNAPSHOT</version>
 | 
				
			||||||
 | 
					    </parent>
 | 
				
			||||||
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <artifactId>concord-server</artifactId>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependencies>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
					            <artifactId>concord-core</artifactId>
 | 
				
			||||||
 | 
					            <version>${project.parent.version}</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.dizitart</groupId>
 | 
				
			||||||
 | 
					            <artifactId>nitrite</artifactId>
 | 
				
			||||||
 | 
					            <version>3.4.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <build>
 | 
				
			||||||
 | 
					        <plugins>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-compiler-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>3.8.1</version>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <annotationProcessorPaths>
 | 
				
			||||||
 | 
					                        <path>
 | 
				
			||||||
 | 
					                            <groupId>org.projectlombok</groupId>
 | 
				
			||||||
 | 
					                            <artifactId>lombok</artifactId>
 | 
				
			||||||
 | 
					                            <version>1.18.20</version>
 | 
				
			||||||
 | 
					                        </path>
 | 
				
			||||||
 | 
					                    </annotationProcessorPaths>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
 | 
					        </plugins>
 | 
				
			||||||
 | 
					    </build>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					module concord_server {
 | 
				
			||||||
 | 
						requires nitrite;
 | 
				
			||||||
 | 
						requires static lombok;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requires java.base;
 | 
				
			||||||
 | 
						requires java.logging;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requires concord_core;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,99 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.extern.java.Log;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Serializer;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Identification;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.ServerWelcome;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.DataInputStream;
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.net.Socket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This thread is responsible for handling the connection to a single client of
 | 
				
			||||||
 | 
					 * a server.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Log
 | 
				
			||||||
 | 
					public class ClientThread extends Thread {
 | 
				
			||||||
 | 
						private final Socket socket;
 | 
				
			||||||
 | 
						private final DataInputStream in;
 | 
				
			||||||
 | 
						private final DataOutputStream out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final ConcordServer server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private Long clientId = null;
 | 
				
			||||||
 | 
						@Getter
 | 
				
			||||||
 | 
						private String clientNickname = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ClientThread(Socket socket, ConcordServer server) throws IOException {
 | 
				
			||||||
 | 
							this.socket = socket;
 | 
				
			||||||
 | 
							this.server = server;
 | 
				
			||||||
 | 
							this.in = new DataInputStream(socket.getInputStream());
 | 
				
			||||||
 | 
							this.out = new DataOutputStream(socket.getOutputStream());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void sendToClient(Message message) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								Serializer.writeMessage(message, this.out);
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void sendToClient(byte[] bytes) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								this.out.write(bytes);
 | 
				
			||||||
 | 
								this.out.flush();
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void run() {
 | 
				
			||||||
 | 
							if (!identifyClient()) {
 | 
				
			||||||
 | 
								log.warning("Could not identify the client; aborting connection.");
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							while (true) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									var msg = Serializer.readMessage(this.in);
 | 
				
			||||||
 | 
									if (msg instanceof Chat chat) {
 | 
				
			||||||
 | 
										this.server.handleChat(chat);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									log.info("Client disconnected: " + e.getMessage());
 | 
				
			||||||
 | 
									if (this.clientId != null) {
 | 
				
			||||||
 | 
										this.server.deregisterClient(this.clientId);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private boolean identifyClient() {
 | 
				
			||||||
 | 
							int attempts = 0;
 | 
				
			||||||
 | 
							while (attempts < 5) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									var msg = Serializer.readMessage(this.in);
 | 
				
			||||||
 | 
									if (msg instanceof Identification id) {
 | 
				
			||||||
 | 
										this.clientId = this.server.registerClient(this);
 | 
				
			||||||
 | 
										this.clientNickname = id.getNickname();
 | 
				
			||||||
 | 
										var reply = new ServerWelcome();
 | 
				
			||||||
 | 
										reply.setClientId(this.clientId);
 | 
				
			||||||
 | 
										Serializer.writeMessage(reply, this.out);
 | 
				
			||||||
 | 
										return true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									e.printStackTrace();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								attempts++;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,75 @@
 | 
				
			||||||
 | 
					package nl.andrewl.concord_server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.extern.java.Log;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.Serializer;
 | 
				
			||||||
 | 
					import nl.andrewl.concord_core.msg.types.Chat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.ByteArrayOutputStream;
 | 
				
			||||||
 | 
					import java.io.DataOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.net.ServerSocket;
 | 
				
			||||||
 | 
					import java.net.Socket;
 | 
				
			||||||
 | 
					import java.security.SecureRandom;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Random;
 | 
				
			||||||
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Log
 | 
				
			||||||
 | 
					public class ConcordServer implements Runnable {
 | 
				
			||||||
 | 
						private final Map<Long, ClientThread> clients = new ConcurrentHashMap<>(32);
 | 
				
			||||||
 | 
						private final int port;
 | 
				
			||||||
 | 
						private final Random random;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ConcordServer(int port) {
 | 
				
			||||||
 | 
							this.port = port;
 | 
				
			||||||
 | 
							this.random = new SecureRandom();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public long registerClient(ClientThread clientThread) {
 | 
				
			||||||
 | 
							long id = this.random.nextLong();
 | 
				
			||||||
 | 
							log.info("Registering new client " + clientThread.getClientNickname() + " with id " + id);
 | 
				
			||||||
 | 
							this.clients.put(id, clientThread);
 | 
				
			||||||
 | 
							return id;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void deregisterClient(long clientId) {
 | 
				
			||||||
 | 
							this.clients.remove(clientId);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void handleChat(Chat chat) {
 | 
				
			||||||
 | 
							System.out.println(chat.getSenderNickname() + ": " + chat.getMessage());
 | 
				
			||||||
 | 
							ByteArrayOutputStream baos = new ByteArrayOutputStream(chat.getByteCount());
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								Serializer.writeMessage(chat, new DataOutputStream(baos));
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							byte[] data = baos.toByteArray();
 | 
				
			||||||
 | 
							for (var client : clients.values()) {
 | 
				
			||||||
 | 
								client.sendToClient(data);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void run() {
 | 
				
			||||||
 | 
							ServerSocket serverSocket;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								serverSocket = new ServerSocket(this.port);
 | 
				
			||||||
 | 
								log.info("Opened server on port " + this.port);
 | 
				
			||||||
 | 
								while (true) {
 | 
				
			||||||
 | 
									Socket socket = serverSocket.accept();
 | 
				
			||||||
 | 
									log.info("Accepted new socket connection.");
 | 
				
			||||||
 | 
									ClientThread clientThread = new ClientThread(socket, this);
 | 
				
			||||||
 | 
									clientThread.start();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void main(String[] args) {
 | 
				
			||||||
 | 
							var server = new ConcordServer(8123);
 | 
				
			||||||
 | 
							server.run();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<configuration>
 | 
				
			||||||
 | 
					    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 | 
				
			||||||
 | 
					        <encoder>
 | 
				
			||||||
 | 
					            <pattern>%d{dd.MM.yyyy HH:mm:ss} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) %highlight(%-6level) %msg%n</pattern>
 | 
				
			||||||
 | 
					        </encoder>
 | 
				
			||||||
 | 
					    </appender>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <root level="info">
 | 
				
			||||||
 | 
					        <appender-ref ref="STDOUT" />
 | 
				
			||||||
 | 
					    </root>
 | 
				
			||||||
 | 
					</configuration>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue