Improved readme, added JNA for windows terminal support.
This commit is contained in:
parent
1a923e0ff8
commit
009c3a7c21
36
README.md
36
README.md
|
@ -1,11 +1,45 @@
|
|||
# Concord
|
||||
Console-based real-time messaging platform, inspired by Discord.
|
||||
|
||||
This platform is organized by many independent servers, each of which supports the following:
|
||||
## Vision
|
||||
|
||||
This platform will be organized by many independent servers, each of which will support 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.
|
||||
|
||||
# Concord Client
|
||||
|
||||
To use the client, simply download the latest `concord-client.jar` JAR file from [the releases page](https://github.com/andrewlalis/Concord/releases) and run it with Java (version 16 or higher) from anywhere on your machine.
|
||||
|
||||
Once you've started it, press **Enter** to click the button "Connect to Server". You will be prompted for the server IP address, and then a nickname for yourself, before you join the server. To disconnect, press **CTRL+C** at any time.
|
||||
|
||||
# Concord Server
|
||||
|
||||
To start up your own server, download the latest `concord-server.jar` JAR file from [the releases page](https://github.com/andrewlalis/Concord/releases) and run it with Java (version 16 or higher). The first time you run the server with `java -jar concord-server.jar`, it will generate a `server-config.json` configuration file, and a `concord-server.db` database file.
|
||||
|
||||
## Configuring the Server
|
||||
|
||||
You probably want to customize your server a bit. To do so, first stop your server by typing `stop` in the console where you started the server initially. Now you can edit `server-config.json` and restart the server once you're done. A description of the attributes is given below:
|
||||
|
||||
- `name` The name of the server.
|
||||
- `description` A short description of what this server is for, or who it's run by.
|
||||
- `port` The port on which the server accepts client connections.
|
||||
- `chatHistoryMaxCount` The maximum amount of chat messages that a client can request from the server at any given time. Decrease this to improve performance.
|
||||
- `chatHistoryDefaultCount` The default number of chat messages that are provided to clients when they join a channel, if they don't explicitly request a certain amount. Decrease this to improve performance.
|
||||
- `maxMessageLength` The maximum length of a message. Messages longer than this will be rejected.
|
||||
- `channels` Contains a list of all channels that the server uses. Each channel has an `id`, `name`, and `description`. **It is advised that you do not add or remove channels manually!** Instead, use the `add-channel` and `remove-channel` CLI commands that are available while the server is running.
|
||||
- `discoveryServers` A list of URLs to which this server should send its metadata for publishing. Keep this empty if you don't want your server to be publicly visible.
|
||||
|
||||
## Server CLI
|
||||
|
||||
As mentioned briefly, the server supports a basic command-line-interface with some commands. You can show which commands are available via the `help` command. The following is a list of some of the most useful commands and a description of their functionality:
|
||||
|
||||
- `add-channel <name>` Adds a new channel to the server with the given name. Channel names cannot be blank, and they cannot be duplicates of an existing channel name.
|
||||
- `remove-channel <name>` Removes a channel.
|
||||
- `list-clients` Shows a list of all connected clients.
|
||||
- `stop` Stops the server, disconnecting all clients.
|
||||
|
||||
Each server uses a single [Nitrite](https://www.dizitart.org/nitrite-database/#what-is-nitrite) database to hold messages and other information.
|
||||
|
|
|
@ -23,6 +23,16 @@
|
|||
<artifactId>lanterna</artifactId>
|
||||
<version>3.2.0-alpha1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna-platform</artifactId>
|
||||
<version>5.9.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module concord_client {
|
||||
requires concord_core;
|
||||
requires com.googlecode.lanterna;
|
||||
requires com.sun.jna;
|
||||
requires com.sun.jna.platform;
|
||||
requires static lombok;
|
||||
}
|
|
@ -17,6 +17,15 @@ public class ChannelManager {
|
|||
this.server = server;
|
||||
this.channelNameMap = new ConcurrentHashMap<>();
|
||||
this.channelIdMap = new ConcurrentHashMap<>();
|
||||
// Initialize the channels according to what's defined in the server's config.
|
||||
for (var channelConfig : server.getConfig().getChannels()) {
|
||||
this.addChannel(new Channel(
|
||||
server,
|
||||
UUID.fromString(channelConfig.getId()),
|
||||
channelConfig.getName(),
|
||||
server.getDb().getCollection("channel-" + channelConfig.getId())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Channel> getChannels() {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
package nl.andrewl.concord_server;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.Getter;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
import nl.andrewl.concord_core.msg.Serializer;
|
||||
|
@ -16,15 +13,9 @@ import org.dizitart.no2.Nitrite;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -36,45 +27,62 @@ import java.util.stream.Collectors;
|
|||
* The main server implementation, which handles accepting new clients.
|
||||
*/
|
||||
public class ConcordServer implements Runnable {
|
||||
private static final Path CONFIG_FILE = Path.of("server-config.json");
|
||||
private static final Path DATABASE_FILE = Path.of("concord-server.db");
|
||||
|
||||
private final Map<UUID, ClientThread> clients;
|
||||
private volatile boolean running;
|
||||
private final ServerSocket serverSocket;
|
||||
|
||||
/**
|
||||
* Server configuration data. This is used to define channels, discovery
|
||||
* server addresses, and more.
|
||||
*/
|
||||
@Getter
|
||||
private final ServerConfig config;
|
||||
|
||||
/**
|
||||
* The component that generates new user and channel ids.
|
||||
*/
|
||||
@Getter
|
||||
private final IdProvider idProvider;
|
||||
|
||||
/**
|
||||
* The database that contains all messages and other server information.
|
||||
*/
|
||||
@Getter
|
||||
private final Nitrite db;
|
||||
|
||||
/**
|
||||
* A general-purpose executor service that can be used to submit async tasks.
|
||||
*/
|
||||
@Getter
|
||||
private final ExecutorService executorService;
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
|
||||
/**
|
||||
* Manager that handles incoming messages and events by clients.
|
||||
*/
|
||||
@Getter
|
||||
private final EventManager eventManager;
|
||||
|
||||
/**
|
||||
* Manager that handles the collection of channels in this server.
|
||||
*/
|
||||
@Getter
|
||||
private final ChannelManager channelManager;
|
||||
|
||||
// Components for communicating with discovery servers.
|
||||
private final DiscoveryServerPublisher discoveryServerPublisher;
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public ConcordServer() {
|
||||
public ConcordServer() throws IOException {
|
||||
this.idProvider = new UUIDProvider();
|
||||
this.config = ServerConfig.loadOrCreate(Path.of("server-config.json"), idProvider);
|
||||
this.db = Nitrite.builder()
|
||||
.filePath("concord-server.db")
|
||||
.openOrCreate();
|
||||
this.config = ServerConfig.loadOrCreate(CONFIG_FILE, idProvider);
|
||||
this.discoveryServerPublisher = new DiscoveryServerPublisher(this.config);
|
||||
this.db = Nitrite.builder().filePath(DATABASE_FILE.toFile()).openOrCreate();
|
||||
this.clients = new ConcurrentHashMap<>(32);
|
||||
this.executorService = Executors.newCachedThreadPool();
|
||||
this.eventManager = new EventManager(this);
|
||||
this.channelManager = new ChannelManager(this);
|
||||
for (var channelConfig : config.getChannels()) {
|
||||
this.channelManager.addChannel(new Channel(
|
||||
this,
|
||||
UUID.fromString(channelConfig.getId()),
|
||||
channelConfig.getName(),
|
||||
this.db.getCollection("channel-" + channelConfig.getId())
|
||||
));
|
||||
}
|
||||
this.serverSocket = new ServerSocket(this.config.getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,10 +123,29 @@ public class ConcordServer implements Runnable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the server is currently running, meaning it is accepting
|
||||
* connections, or false otherwise.
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the server. Has no effect if the server has not started yet or has
|
||||
* already been stopped.
|
||||
*/
|
||||
public void stop() {
|
||||
this.running = false;
|
||||
if (!this.serverSocket.isClosed()) {
|
||||
try {
|
||||
this.serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<UserData> getClients() {
|
||||
return this.clients.values().stream()
|
||||
.sorted(Comparator.comparing(ClientThread::getClientNickname))
|
||||
|
@ -153,64 +180,54 @@ public class ConcordServer implements Runnable {
|
|||
}
|
||||
}
|
||||
|
||||
private void publishMetaDataToDiscoveryServers() {
|
||||
if (this.config.getDiscoveryServers().isEmpty()) return;
|
||||
ObjectNode node = this.mapper.createObjectNode();
|
||||
node.put("name", this.config.getName());
|
||||
node.put("description", this.config.getDescription());
|
||||
node.put("port", this.config.getPort());
|
||||
String json;
|
||||
try {
|
||||
json = this.mapper.writeValueAsString(node);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
/**
|
||||
* Shuts down the server cleanly by doing the following things:
|
||||
* <ol>
|
||||
* <li>Disconnecting all clients.</li>
|
||||
* <li>Shutting down any executor services.</li>
|
||||
* <li>Flushing and compacting the message database.</li>
|
||||
* <li>Flushing the server configuration one last time.</li>
|
||||
* </ol>
|
||||
*/
|
||||
private void shutdown() {
|
||||
System.out.println("Shutting down the server.");
|
||||
for (var clientId : this.clients.keySet()) {
|
||||
this.deregisterClient(clientId);
|
||||
}
|
||||
var discoveryServers = List.copyOf(this.config.getDiscoveryServers());
|
||||
for (var discoveryServer : discoveryServers) {
|
||||
System.out.println("Publishing this server's metadata to discovery server at " + discoveryServer);
|
||||
var request = HttpRequest.newBuilder(URI.create(discoveryServer))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.build();
|
||||
this.scheduledExecutorService.shutdown();
|
||||
this.executorService.shutdown();
|
||||
this.db.close();
|
||||
try {
|
||||
this.httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
||||
} catch (IOException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.config.save();
|
||||
} catch (IOException e) {
|
||||
System.err.println("Could not save configuration on shutdown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.running = true;
|
||||
this.scheduledExecutorService.scheduleAtFixedRate(this::publishMetaDataToDiscoveryServers, 0, 1, TimeUnit.MINUTES);
|
||||
ServerSocket serverSocket;
|
||||
try {
|
||||
serverSocket = new ServerSocket(this.config.getPort());
|
||||
this.scheduledExecutorService.scheduleAtFixedRate(this.discoveryServerPublisher::publish, 0, 1, TimeUnit.MINUTES);
|
||||
StringBuilder startupMessage = new StringBuilder();
|
||||
startupMessage.append("Opened server on port ").append(config.getPort()).append("\n");
|
||||
startupMessage.append("Opened server on port ").append(config.getPort()).append("\n")
|
||||
.append("The following channels are available:\n");
|
||||
for (var channel : this.channelManager.getChannels()) {
|
||||
startupMessage.append("\tChannel \"").append(channel).append('\n');
|
||||
}
|
||||
System.out.println(startupMessage);
|
||||
while (this.running) {
|
||||
try {
|
||||
Socket socket = serverSocket.accept();
|
||||
Socket socket = this.serverSocket.accept();
|
||||
ClientThread clientThread = new ClientThread(socket, this);
|
||||
clientThread.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("Could not accept new client connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("Could not open server socket: " + e.getMessage());
|
||||
}
|
||||
this.scheduledExecutorService.shutdown();
|
||||
this.shutdown();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
public static void main(String[] args) throws IOException {
|
||||
var server = new ConcordServer();
|
||||
new Thread(server).start();
|
||||
new ServerCli(server).run();
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package nl.andrewl.concord_server;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import nl.andrewl.concord_server.config.ServerConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This component is responsible for publishing the server's metadata to any
|
||||
* discovery servers that have been defined in the server's configuration file.
|
||||
*/
|
||||
public class DiscoveryServerPublisher {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
private final ServerConfig config;
|
||||
|
||||
public DiscoveryServerPublisher(ServerConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void publish() {
|
||||
if (this.config.getDiscoveryServers().isEmpty()) return;
|
||||
ObjectNode node = this.mapper.createObjectNode();
|
||||
node.put("name", this.config.getName());
|
||||
node.put("description", this.config.getDescription());
|
||||
node.put("port", this.config.getPort());
|
||||
String json;
|
||||
try {
|
||||
json = this.mapper.writeValueAsString(node);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
var discoveryServers = List.copyOf(this.config.getDiscoveryServers());
|
||||
for (var discoveryServer : discoveryServers) {
|
||||
var request = HttpRequest.newBuilder(URI.create(discoveryServer))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.build();
|
||||
try {
|
||||
this.httpClient.send(request, HttpResponse.BodyHandlers.discarding());
|
||||
} catch (IOException | InterruptedException e) {
|
||||
System.err.println("Could not publish metadata to " + discoveryServer + " because of exception: " + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import nl.andrewl.concord_server.ConcordServer;
|
|||
import nl.andrewl.concord_server.cli.command.AddChannelCommand;
|
||||
import nl.andrewl.concord_server.cli.command.ListClientsCommand;
|
||||
import nl.andrewl.concord_server.cli.command.RemoveChannelCommand;
|
||||
import nl.andrewl.concord_server.cli.command.StopCommand;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
@ -22,10 +23,19 @@ public class ServerCli implements Runnable {
|
|||
this.commands.put("list-clients", new ListClientsCommand());
|
||||
this.commands.put("add-channel", new AddChannelCommand());
|
||||
this.commands.put("remove-channel", new RemoveChannelCommand());
|
||||
this.commands.put("stop", new StopCommand());
|
||||
|
||||
this.commands.put("help", (s, args) -> {
|
||||
System.out.println("The following commands are available:");
|
||||
for (var key : commands.keySet().stream().sorted().toList()) {
|
||||
System.out.println("\t" + key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("Server command-line-interface initialized. Type \"help\" for a list of available commands.");
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
||||
String line;
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package nl.andrewl.concord_server.cli.command;
|
||||
|
||||
import nl.andrewl.concord_server.ConcordServer;
|
||||
import nl.andrewl.concord_server.cli.ServerCliCommand;
|
||||
|
||||
/**
|
||||
* This command forcibly stops the server, disconnecting any clients.
|
||||
*/
|
||||
public class StopCommand implements ServerCliCommand {
|
||||
@Override
|
||||
public void handle(ConcordServer server, String[] args) throws Exception {
|
||||
server.stop();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue