Compare commits

..

49 Commits

Author SHA1 Message Date
Clément
dfdaae163b documentation for all major modules 2025-03-12 23:16:39 +01:00
Clément
0b6f5193a0 wait animation for loading 2025-03-12 23:04:35 +01:00
Clément
4bb45cc3db tag + basic layout for rooms 2025-03-12 22:55:31 +01:00
Clément
39a65e1ca6 colored message (with evil regex) 2025-03-12 22:17:17 +01:00
Clément
2b17891379 working and prettier chat 2025-03-12 22:01:30 +01:00
Clément
bfd3c10e9e all functionality handled 2025-03-12 21:34:55 +01:00
Clément
11fc2e2ac8 added main components for joining and leaving rooms 2025-03-12 15:29:03 +01:00
Clément
2be11ec4a8 It works ! 2025-03-12 14:26:13 +01:00
3eec95e420 ne pas regarder 2025-03-12 13:18:27 +01:00
0900113bb8 add doc 2025-03-12 12:56:45 +01:00
Clément
07272732d4 Pass Client & ClientListener through all controllers 2025-03-10 13:52:35 +01:00
3abdc09819 generic signals 2025-03-10 13:51:48 +01:00
980527f45f mieux 2025-03-10 12:42:40 +01:00
Clément
a2a5e96dc5 forgot to do that 2025-03-07 11:25:36 +01:00
Clément
3ad5bcf819 README update 2025-03-07 11:24:40 +01:00
462307dabc network: process time out 2025-03-06 12:02:42 +01:00
72c62bb1b4 network: remove ack by default 2025-03-06 11:48:59 +01:00
c8a748fe71 uggly reliable 2025-03-06 11:46:45 +01:00
Clément
6950810b95 Basic structure for GUI 2025-03-05 23:29:58 +01:00
Clément
f87145ed69 styling and transition with login page 2025-03-05 18:38:54 +01:00
Clément
5cefe42a99 Login page 2025-03-05 18:08:26 +01:00
Clément
6d228aee55 Icon + loader for client 2025-03-05 12:55:35 +01:00
Clément
a83104d322 ServerGui view 2025-03-05 12:06:27 +01:00
583505d93a indent file 2025-03-04 19:31:16 +01:00
0a8006fd56 no more tasks 2025-03-04 19:30:42 +01:00
Clément
0560d23cd3 Added forgotten fxml 2025-03-04 19:19:47 +01:00
Clément
7866984e19 added gui for server 2025-03-04 19:11:21 +01:00
Clément
5669859ac1 Update README.md 2025-03-04 18:18:41 +01:00
2bb3e64f2b big socket refactor 2025-03-04 16:32:11 +01:00
76da347fb9 client gui + headless server 2025-03-04 16:07:35 +01:00
Clément
e536a45266 Migrate everything on Gradle
Also added tasks to run the server & client separately
2025-03-04 15:08:43 +01:00
3115d397a4 update Sudoku submodule 2025-03-03 22:02:18 +01:00
a5a41f573b remove unused imports 2025-03-03 22:01:52 +01:00
8f30f139cd better command interface 2025-03-03 21:29:54 +01:00
Clément
c83e39ea4b preventing regex injection 2025-03-03 12:23:44 +01:00
Clément
5befdd3080 Added tag feature 2025-03-03 12:15:17 +01:00
Clément
4b8adef72f fix to prevent server chat from displaying the handshakes spam 2025-03-02 13:07:09 +01:00
Clément
39a2afcd6e fix for message where there's no room available while typing /room 2025-03-02 12:42:44 +01:00
Clément
c9e564370e Fixed bug allowing client to be in two different rooms at the same time 2025-03-02 12:37:25 +01:00
Clément
e52066ce17 Added /room command to know in which room the user is 2025-03-02 12:28:53 +01:00
Clément
e9f1feaaad Refactor to help further graphics 2025-03-02 11:53:59 +01:00
Clément
7adb581e33 fixed bug when joining room while being in another 2025-03-01 19:56:02 +01:00
Clément
10f6b059b1 escape strings for real tui 2025-03-01 19:50:19 +01:00
Clément
f8f740f799 add command aliases 2025-03-01 14:55:59 +01:00
Clément
a2c4319182 handshaking 2025-03-01 14:52:05 +01:00
90f92281ef leave/join room messages 2025-03-01 13:26:09 +01:00
0533c16cf2 refactor server room operations 2025-03-01 13:06:55 +01:00
63ec7b3aaa add disconnect 2025-03-01 13:00:58 +01:00
07ad2ba05e ClientListener + ClientConsole 2025-03-01 12:41:40 +01:00
65 changed files with 2607 additions and 524 deletions

7
ChatApp/.gitignore vendored
View File

@@ -1,3 +1,8 @@
.vscode
bin
lib
lib
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build

View File

@@ -3,15 +3,56 @@ An instant messaging app using Java and the UDP protocol.
## How to run
> [!WARNING]
> As of now, the app rely on ANSI escape character, thus not available on Windows.
> However, this feature is under development.
### Console
You can run the app by launching the `ChatApp` class. This will create the server (port `6665`) and a first client,
(creating by the way a room (`101`)).
#### Server + Client
You can then create more clients by launching the `Client` class. It will connect to the server
(be sure you launched it first) on the default port `6665`.
You can create a server and an "admin" client by using:
```shell
./gradlew admin # Linux
.\gradlew.bat admin # Windows
```
You will receive the notifications of the server (handshakes, ...).
#### Server
You can create a server by launching:
```shell
./gradlew server # Linux
.\gradlew server # Windows
```
If you want to create a server on a specific port, you can use:
```shell
./gradlew server --args="port" # Linux
.\gradlew server --args="port" # Windows
```
where `port` is the port you want to use.
This will create the server on a random port available and indicate it through the terminal.
To launch the server on a particular port, simply use:
```shell
./gradlew server --args="port" # Linux
.\gradlew server --args="port" # Windows
```
where `port` is the port you want to use.
#### Client
To create a client, you may use:
```shell
./gradlew client # Linux
.\gradlew client # Windows
```
## How to use
As soon as you launch a client, you will be prompted to enter your name. You will then be in the lobby. From here you
@@ -29,6 +70,8 @@ You will also be able to create a new room.
- /listRooms
- /joinRoom *roomName*
- /leaveRoom
- /room
- /bye
- /help
> [!NOTE]

62
ChatApp/app/build.gradle Normal file
View File

@@ -0,0 +1,62 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.13/userguide/building_java_projects.html in the Gradle documentation.
*/
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use JUnit Jupiter for testing.
testImplementation libs.junit.jupiter
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// This dependency is used by the application.
implementation libs.guava
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
javafx {
modules = [ 'javafx.graphics', 'javafx.controls', 'javafx.fxml' ]
}
application {
// Define the main class for the application.
if (project.hasProperty('admin')) {
mainClass = 'ChatApp'
} else if (project.hasProperty('server')) {
mainClass = 'ChatAppServer'
} else if (project.hasProperty('client')) {
mainClass = 'ChatAppClient'
} else if (project.hasProperty('serverGui')) {
mainClass = 'server.ServerGui'
} else {
mainClass = 'client.ClientGui'
}
}
run {
standardInput = System.in
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@@ -0,0 +1,23 @@
import java.net.InetSocketAddress;
import client.ClientConsole;
import server.Server;
public class ChatApp {
public static void main(String[] args) throws Exception {
Server server = new Server(6665);
ClientConsole client = new ClientConsole(new InetSocketAddress("localhost", 6665));
client.onConnect.connect((arg) -> {
client.getClientInterface().SendCreateRoom("101");
});
client.onDisconnect.connect((arg) -> {
System.out.println("Stopping server ...");
server.close();
System.out.println("Done !");
});
}
}

View File

@@ -0,0 +1,15 @@
import client.ClientConsole;
import java.net.InetSocketAddress;
public class ChatAppClient {
public static void main(String[] args) {
ClientConsole console = new ClientConsole(new InetSocketAddress("localhost", 6665));
try {
console.joinThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End !");
}
}

View File

@@ -0,0 +1,15 @@
import java.net.SocketException;
import server.Server;
public class ChatAppServer {
public static void main(String[] args) {
// run ./gradlew server --args="port" to launch the server with a specific port
try {
Server server = new Server(args.length > 0 ? Integer.parseInt(args[0]) : 0);
System.out.println("Server running on port " + server.getRunningPort() + "!");
} catch (SocketException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,58 @@
package client;
import java.net.InetSocketAddress;
import java.net.SocketException;
import network.Socket;
import network.protocol.packets.*;
public class Client {
private final ClientConnexion connexion;
private ClientListener callback;
public Client(InetSocketAddress serverAddress, ClientListener callback, String pseudo) throws SocketException {
this.connexion = new ClientConnexion(new Socket(), serverAddress, callback);
this.callback = callback;
login(pseudo);
}
private void login(String pseudo) {
this.connexion.sendPacket(new LoginPacket(pseudo));
}
public void close() {
this.connexion.sendPacket(new DisconnectPacket("Leaving"));
this.connexion.close();
this.callback.handleDisconnect();
}
public void SendChatMessage(String message) {
this.connexion.sendPacket(new SendChatMessagePacket(message));
}
public void SendCreateRoom(String roomName) {
this.connexion.sendPacket(new CreateRoomPacket(roomName));
}
public void SendJoinRoom(String roomName) {
this.connexion.sendPacket(new JoinRoomPacket(roomName));
}
public void SendLeaveRoom() {
this.connexion.sendPacket(new LeaveRoomPacket());
}
public void RequestRoomList() {
this.connexion.sendPacket(new RequestRoomListPacket());
}
public void RequestActualRoom() {
this.connexion.sendPacket(new RequestActualRoomPacket());
}
public void changeCallback(ClientListener callback) {
this.callback = callback;
this.connexion.changeCallback(callback);
}
}

View File

@@ -1,36 +1,45 @@
package client;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.PacketHandler;
import network.SocketReader;
import network.SocketWriter;
import network.protocol.ANSIColor;
import network.Socket;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.*;
import network.protocol.packets.ServerResponsePacket.Response;
public class ClientConnexion implements PacketVisitor, PacketHandler{
public class ClientConnexion implements PacketVisitor, PacketHandler {
private final InetSocketAddress serverAddress;
private final SocketWriter writer;
private final SocketReader reader;
protected boolean connected = false;
private final Socket socket;
private ClientListener callback;
public ClientConnexion(DatagramSocket socket, InetSocketAddress serverAddress) {
public ClientConnexion(Socket socket, InetSocketAddress serverAddress, ClientListener callback) {
this.serverAddress = serverAddress;
this.writer = new SocketWriter(socket);
this.reader = new SocketReader(socket, this);
this.socket = socket;
this.callback = callback;
this.socket.onClose.connect((args) -> onSocketClose());
this.socket.addHandler(this);
}
private void onSocketClose() {
this.callback.handleConnexionError();
}
public void close() {
this.reader.stop();
this.socket.close();
}
public void sendPacket(Packet packet) throws IOException {
this.writer.sendPacket(packet, serverAddress);
public void sendPacket(Packet packet) {
try {
this.socket.sendPacket(packet, serverAddress);
} catch (IOException e) {
this.close();
this.callback.handleConnexionError();
e.printStackTrace();
}
}
@Override
@@ -41,16 +50,39 @@ public class ClientConnexion implements PacketVisitor, PacketHandler{
@Override
public void visitPacket(ChatMessagePacket packet) {
StringBuilder sb = new StringBuilder();
String time = packet.getTime().toString();
sb.append("&y[")
.append(time, 11, 19) // We only take the HH:MM:SS part
.append("]&n")
.append(" ")
.append(packet.getChatter())
.append(" : ")
.append(packet.getContent()).append("&n"); // make the color back to normal at the end of every message
System.out.println(ANSIColor.formatString(sb.toString()));
this.callback.handleChatMessage(packet.getTime(), packet.getChatter(), packet.getContent());
}
@Override
public void visitPacket(RoomListPacket packet) {
this.callback.handleRoomList(packet.getRoomNames());
}
@Override
public void visitPacket(ServerResponsePacket packet) {
this.callback.handleServerResponse(packet.getResponse());
if (packet.getResponse() == Response.AuthSuccess)
this.callback.handleConnect();
}
@Override
public void visitPacket(ActualRoomPacket packet) {
this.callback.handleActualRoom(packet.getRoomName());
}
@Override
public void visitPacket(DisconnectPacket packet) {
this.close();
this.callback.handleDisconnect();
}
public void changeCallback(ClientListener callback) {
this.callback = callback;
}
@Override
public void visitPacket(RequestActualRoomPacket packet) {
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
@@ -78,29 +110,8 @@ public class ClientConnexion implements PacketVisitor, PacketHandler{
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(RoomListPacket packet) {
System.out.println("Rooms :");
for (String room : packet.getRoomNames()) {
System.out.println("\t" + room);
}
}
@Override
public void visitPacket(SendChatMessagePacket packet) {
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ServerResponsePacket packet) {
if(packet.getResponse() == ServerResponsePacket.Response.MessageSent || packet.getResponse() == ServerResponsePacket.Response.MessageNotSent) {
return;
}
System.out.println(packet.getResponse());
}
@Override
public void visitPacket(HandshakePacket packet) {
connected = true;
}
}

View File

@@ -0,0 +1,187 @@
package client;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.function.Consumer;
import network.protocol.ANSIColor;
import network.protocol.packets.ServerResponsePacket;
import network.protocol.packets.ServerResponsePacket.Response;
import utilities.Signal;
public class ClientConsole implements ClientListener {
private Client client;
private final Thread inputThread;
private final Scanner scanner;
public final Signal onConnect = new Signal();
public final Signal onDisconnect = new Signal();
public ClientConsole(InetSocketAddress address) {
this.inputThread = new Thread(this::inputLoop);
this.scanner = new Scanner(System.in);
String pseudo = inputPseudo();
try {
this.client = new Client(address, this, pseudo);
this.inputThread.start();
} catch (SocketException e) {
e.printStackTrace();
}
}
private String inputPseudo() {
System.out.println("Enter your pseudo:");
String pseudo = "chatter";
pseudo = this.scanner.nextLine();
return pseudo;
}
public Client getClientInterface() {
return this.client;
}
private void inputLoop() {
while (!Thread.interrupted()) {
String message = this.scanner.nextLine();
if (Thread.interrupted())
break;
visitMessage(message);
}
}
public void joinThread() throws InterruptedException {
this.inputThread.join();
}
private void printHelp(Map<List<String>, Consumer<String>> commands) {
System.out.println("Available commands:");
for (var entry : commands.entrySet()) {
List<String> commandNames = entry.getKey();
System.out.println("\t" + commandNames.get(0) + (commandNames.size() == 2 ? " or " + commandNames.get(1) : ""));
}
// the clearLine eats the last line if a new line is not skipped
System.out.println("\n");
}
private void visitMessage(String message) {
if (!message.startsWith("/")) {
this.client.SendChatMessage(message);
clearLine();
return;
}
final Map<List<String>, Consumer<String>> commands = Map.of(
List.of("/createRoom <roomName>", "/create <roomName>"), (args) -> this.client.SendCreateRoom(args),
List.of("/listRooms", "/list"), (args) -> this.client.RequestRoomList(),
List.of("/joinRoom <roomName>", "/join <roomName>"), (args) -> this.client.SendJoinRoom(args),
List.of("/leaveRoom", "/leave"), (args) -> this.client.SendLeaveRoom(),
List.of("/actualRoom", "/room"), (args) -> this.client.RequestActualRoom(),
List.of("/goodbye", "/bye"), (args) -> this.client.close(),
List.of("/help"), (args) -> {});
boolean commandFound = false;
for (var entry : commands.entrySet()) {
List<String> commandNames = entry.getKey();
Consumer<String> runnable = entry.getValue();
for (String cmd : commandNames) {
String[] cmdParts = cmd.split(" ");
if (cmdParts.length > 1) {
// the command expects arguments
if (message.startsWith(cmdParts[0] + " ")) {
runnable.accept(message.substring(cmdParts[0].length() + 1).trim());
commandFound = true;
}
} else {
// the command does not expect arguments
if (message.equals(cmd)) {
runnable.accept(message.substring(cmd.length()).trim());
commandFound = true;
}
}
}
if (commandFound)
break;
}
if (commandFound) {
if (message.equals("/help"))
printHelp(commands);
clearLine();
return;
}
System.out.println(ANSIColor.formatString("&rUnknown command&n"));
printHelp(commands);
clearLine();
}
private void clearLine() {
System.out.print("\033[1A\r\033[2K"); // weird sequence to clear the line (but it works !)
System.out.flush();
}
private void stop() {
this.inputThread.interrupt();
}
@Override
public void handleDisconnect() {
System.out.println("Disconnected !");
stop();
this.onDisconnect.emit();
}
@Override
public void handleConnexionError() {
System.out.println("An error occured during the connexion !");
stop();
}
@Override
public void handleChatMessage(Instant time, String chatter, String content) {
StringBuilder sb = new StringBuilder();
String strTime = time.toString();
sb.append("&y[")
.append(strTime, 11, 19) // We only take the HH:MM:SS part
.append("]&n")
.append(" ")
.append(chatter)
.append(" : ")
.append(content).append("&n"); // make the color back to normal at the end of every message
System.out.println(ANSIColor.formatString(sb.toString()));
}
@Override
public void handleRoomList(List<String> roomNames) {
System.out.println(roomNames.isEmpty()
? "No rooms available"
: "Rooms : \n\t" + String.join("\n\t", roomNames));
}
@Override
public void handleActualRoom(String roomName) {
System.out.println(roomName != null ? ANSIColor.formatString("You are now in room &b" + roomName + "&n") : "You are not in a room");
}
@Override
public void handleServerResponse(Response response) {
if (response == ServerResponsePacket.Response.MessageSent
|| response == ServerResponsePacket.Response.MessageNotSent) {
return;
}
System.out.println(response);
}
@Override
public void handleConnect() {
System.out.println("Connected to server !");
this.onConnect.emit();
}
}

View File

@@ -0,0 +1,36 @@
package client;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Screen;
import javafx.stage.Stage;
import java.util.Objects;
public class ClientGui extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/client/clientLogin.fxml"));
Scene scene = new Scene(loader.load(), 400, 240);
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - scene.getWidth() / 2;
double yPos = screenHeight / 2 - scene.getHeight() / 2;
scene.getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
stage.setTitle("Liscord");
stage.setScene(scene);
stage.setX(xPos);
stage.setY(yPos);
stage.show();
}
}

View File

@@ -0,0 +1,253 @@
package client;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import network.protocol.packets.ServerResponsePacket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ClientGuiController implements ClientListener {
private Client client;
private boolean connected = true;
@FXML
private BorderPane vueContainer;
@FXML
private VBox roomList;
@FXML
private VBox chatList;
@FXML
private HBox chatInput;
@FXML
private ScrollPane chatPane;
public void setClient(Client client) {
this.client = client;
}
@FXML
public void initialize() throws SocketException {
client = new Client(new InetSocketAddress("localhost", 6665), this, UsernameSingleton.getInstance().getUsername());
Platform.runLater(() -> {
Stage stage = (Stage) vueContainer.getScene().getWindow();
stage.setResizable(true); // Fixed a bug that made the close button disappear under my Wayland setup (don't know if it's the same for x11)
stage.setOnCloseRequest(event -> {
connected = false;
client.close();
});
chatList.heightProperty().addListener((obs, oldVal, newVal) -> chatPane.setVvalue(1.0));
});
}
@Override
public void handleDisconnect() {
System.out.println("Disconnected");
}
/**
* Create an alert dialog when the connection to the server fails.
*/
@Override
public void handleConnexionError() {
if(connected) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Connection");
alert.setHeaderText("Connection failed");
Optional<ButtonType> res = alert.showAndWait();
if (res.isPresent() && res.get() == ButtonType.OK) {
((Stage) vueContainer.getScene().getWindow()).close();
}
});
}
}
/**
* Create an alert dialog when the connection to the server is successful.
*/
@Override
public void handleConnect() {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Connection");
alert.setHeaderText("Connection to the server successful");
alert.showAndWait();
});
}
/**
* Format and insert the received chat message into the chat list.
* @param time the time the message was sent
* @param chatter the name of the person who sent the message
* @param content the content of the message
*/
@Override
public void handleChatMessage(Instant time, String chatter, String content) {
String untagged = untag(content);
String messageColor = Objects.equals(content, untagged) ? "lightblue" : "lightgreen";
Platform.runLater(() -> {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm")
.withZone(ZoneId.systemDefault());
String timeString = formatter.format(time);
Text chatterText = new Text(chatter + ": ");
chatterText.setStyle("-fx-fill: black; -fx-font-weight: bold;");
TextFlow messageText = formatMessage(chatter, untagged);
TextFlow wholeMessage = new TextFlow(chatterText, messageText);
Text timeText = new Text(" " + timeString);
timeText.setStyle("-fx-fill: gray; -fx-font-size: 10px;");
HBox messageContainer = new HBox(5, wholeMessage, timeText);
messageContainer.setStyle("-fx-background-color: " + messageColor + "; -fx-padding: 8; -fx-background-radius: 5;");
messageContainer.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(messageText, Priority.ALWAYS);
chatList.getChildren().add(messageContainer);
});
}
/**
* Remove the tag from the message (handle ANSI characters encoded by the server)
* @param message the message to untag
* @return the untagged message
*/
public String untag(String message) {
Pattern pattern = Pattern.compile("\u001B\\[44;30m(.*?)\u001B\\[49;39m");
Matcher matcher = pattern.matcher(message);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String taggedName = matcher.group(1);
matcher.appendReplacement(result, taggedName);
}
matcher.appendTail(result);
return result.toString();
}
/**
* Display the list of rooms in the room list.
* @param roomNames the list of room names
*/
@Override
public void handleRoomList(List<String> roomNames) {
Platform.runLater(() -> {
roomList.getChildren().clear();
for (String roomName : roomNames) {
Button button = new Button(roomName);
button.setOnAction(event -> {
client.SendJoinRoom(roomName);
createChatEnv(roomName);
});
roomList.getChildren().add(button);
}
});
}
/**
* Create the chat (message wall + input) environment.
* @param roomName the name of the room
*/
private void createChatEnv(String roomName) {
Platform.runLater(() -> {
chatList.getChildren().clear();
chatInput.getChildren().clear();
Button leaveButton = new Button("Leave room");
leaveButton.setOnAction(event -> {
client.SendLeaveRoom();
chatList.getChildren().clear();
chatInput.getChildren().clear();
});
chatList.getChildren().add(leaveButton);
TextField messageInput = new TextField();
messageInput.setPromptText("Type a message...");
messageInput.getStyleClass().add("message-input");
HBox.setHgrow(messageInput, Priority.ALWAYS);
messageInput.onActionProperty().set(event -> {
client.SendChatMessage(messageInput.getText());
messageInput.clear();
});
chatInput.getChildren().add(messageInput);
Button sendButton = new Button("Send");
sendButton.getStyleClass().add("send-button");
sendButton.setOnAction(event -> {
client.SendChatMessage(messageInput.getText());
messageInput.clear();
});
chatInput.getChildren().add(sendButton);
});
}
/**
* Colorize the message according to the color codes.
* @param chatter the name of the person who sent the message
* @param content the content of the message
* @return the formatted message
*/
private TextFlow formatMessage(String chatter, String content) {
TextFlow textFlow = new TextFlow();
// Evil regex : match every sequence starting with a color code ending with the next color code
Pattern pattern = Pattern.compile("&([rbgyn])([^&]*)");
Matcher matcher = pattern.matcher(content);
int lastIndex = 0;
while (matcher.find()) {
if (matcher.start() > lastIndex) {
textFlow.getChildren().add(new Text(content.substring(lastIndex, matcher.start())));
}
String colorCode = matcher.group(1);
String textPart = matcher.group(2);
String color = switch (colorCode) {
case "r" -> "red";
case "b" -> "blue";
case "g" -> "green";
case "y" -> "gray";
default -> "black";
};
Text coloredText = new Text(textPart);
coloredText.setStyle("-fx-fill: " + color + ";");
textFlow.getChildren().add(coloredText);
lastIndex = matcher.end();
}
// Add the remaining text (works if there's no color code at all)
if (lastIndex < content.length()) {
textFlow.getChildren().add(new Text(content.substring(lastIndex)));
}
return textFlow;
}
@Override
public void handleServerResponse(ServerResponsePacket.Response response) {
}
@Override
public void handleActualRoom(String roomName) {
}
}

View File

@@ -0,0 +1,18 @@
package client;
import java.time.Instant;
import java.util.List;
import network.protocol.packets.ServerResponsePacket;
public interface ClientListener {
void handleDisconnect();
void handleConnexionError();
void handleConnect();
void handleChatMessage(Instant time, String chatter, String content);
void handleRoomList(List<String> roomNames);
void handleServerResponse(ServerResponsePacket.Response response);
void handleActualRoom(String roomName);
}

View File

@@ -0,0 +1,55 @@
package client;
import javafx.animation.PauseTransition;
import javafx.animation.RotateTransition;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.shape.Arc;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.io.IOException;
import static utilities.FxUtilities.centerStage;
public class ClientLoading {
@FXML
private Arc spinnerArc;
/**
* Initialize the loading screen.
*/
public void initialize() {
// Rotation animation cycle
RotateTransition rotate = new RotateTransition(Duration.seconds(1), spinnerArc);
rotate.setByAngle(360);
rotate.setCycleCount(RotateTransition.INDEFINITE);
rotate.play();
// Yes, this loading screen is just for show. But it's fun and pretty.
// PauseTransition allows no to block the rotation animation.
PauseTransition pause = new PauseTransition(Duration.millis(1000));
pause.setOnFinished(event -> {
try {
login();
} catch (IOException e) {
e.printStackTrace();
}
});
pause.play();
}
/**
* Load the client GUI and switch to it.
* @throws IOException if the FXML file is not found.
*/
public void login() throws IOException {
var loader = new FXMLLoader(getClass().getResource("/client/clientVue.fxml"));
loader.load();
Stage stage = (Stage) spinnerArc.getScene().getWindow();
stage.setScene(new Scene(loader.getRoot(), 800, 600));
centerStage(stage);
stage.getScene().getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
}
}

View File

@@ -0,0 +1,33 @@
package client;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import java.io.IOException;
import static utilities.FxUtilities.centerStage;
public class ClientLogin {
@FXML
public TextField usernameField;
/**
* Sets the username and switches to the loading screen.
* @throws IOException if the FXML file is not found.
*/
@FXML
private void login() throws IOException {
String username = usernameField.getText();
if(username.isEmpty()) {
return;
}
UsernameSingleton.getInstance().setUsername(username);
var loader = new FXMLLoader(getClass().getResource("/client/clientLoading.fxml"));
loader.load();
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setScene(new Scene(loader.getRoot(), 800, 600));
centerStage(stage);
}
}

View File

@@ -0,0 +1,31 @@
package client;
/**
* This class aims to make the username available to all controllers. (Recommended by JavaFX, because otherwise, FXMLLoader bugs and doesn't switch scenes properly.)
*/
public class UsernameSingleton {
private static UsernameSingleton instance;
private String username;
private UsernameSingleton() {
}
/**
* Get the instance of the singleton.
* @return the instance
*/
public static UsernameSingleton getInstance() {
if (instance == null) {
instance = new UsernameSingleton();
}
return instance;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -0,0 +1,35 @@
package network;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.net.InetAddress;
public class IPAddressFinder {
public static String findIPAddress(){
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (!inetAddress.isLoopbackAddress()) {
String ip = inetAddress.getHostAddress();
if (ip.startsWith("192.168")) {
return ip;
}
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "Not Found";
}
}

View File

@@ -0,0 +1,340 @@
package network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Stack;
import network.protocol.Packet;
import network.protocol.packets.AcknowlegdementPacket;
import network.protocol.packets.DisconnectPacket;
public class PacketPool {
private final Stack<ReliablePacketAddress> packetQueue;
private final Map<InetSocketAddress, AdressContext> addressContexts;
private final Socket socket;
private static int MAX_SEND_TRY = 50;
private static long SEND_DELAY = 10;
private static long RETRY_INTERVAL = SEND_DELAY * 2;
private static float PACKET_LOSS_PROBABILITY = 0.1f;
private static record ReliablePacketAddress(ReliablePacket packet, InetSocketAddress address) {
@Override
public final int hashCode() {
return Objects.hash(packet, address);
}
@Override
public final boolean equals(Object arg0) {
if (arg0 instanceof ReliablePacketAddress packetAddress)
return packetAddress.address().equals(this.address())
&& packetAddress.packet().getSeq() == this.packet().getSeq();
return false;
}
}
/**
* Data specific to one connexion
*/
private static class AdressContext {
public int currentSeqRecv = 0;
public int currentSeqSend = 0;
public final List<Thread> sendThreads = new ArrayList<>();
public final List<ReliablePacket> packetRecvBuffer = new ArrayList<>();
public final Map<ReliablePacket, Integer> packetsSentTries = new HashMap<>();
}
/**
* @param address the address to send to
* @return The next send sequence
*/
private int getNextSeqSend(InetSocketAddress address) {
return this.addressContexts.get(address).currentSeqSend++;
}
/**
* @param address the address which a packet was recieved
* @return the current recieve sequence number
*/
private int getCurrentSeqRecv(InetSocketAddress address) {
return this.addressContexts.get(address).currentSeqRecv;
}
/**
* Set the recieve sequence number
* @param address
* @param newValue
*/
private void setSeqRecv(InetSocketAddress address, int newValue) {
this.addressContexts.get(address).currentSeqRecv = newValue;
}
/**
* Try to add the address into memory
* @param address
*/
private void tryAddContext(InetSocketAddress address) {
this.addressContexts.putIfAbsent(address, new AdressContext());
}
/**
* Construct a PacketPool
* @param socket
*/
public PacketPool(Socket socket) {
this.socket = socket;
this.packetQueue = new Stack<>();
this.addressContexts = new HashMap<>();
}
private void debugPrint(String msg) {
// System.out.println(msg);
}
private void debugSend(ReliablePacket packet, InetSocketAddress address) {
boolean client = address.getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Sent " + packet.getPacket().getClass().getName()
+ " with seq : " + packet.getSeq());
}
private void debugRecv(ReliablePacket packet, InetSocketAddress address) {
boolean client = address.getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Received " + packet.getPacket().getClass().getName()
+ " with seq : " + packet.getSeq());
}
/**
* Send a packet to the socket
* @param packet
* @param address
* @throws IOException
*/
private void sendPacketToSocket(ReliablePacket packet, InetSocketAddress address) throws IOException {
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
new Thread(() -> {
try {
Thread.sleep(SEND_DELAY);
if (Math.random() > PACKET_LOSS_PROBABILITY)
this.socket.sendPacket(packet, address);
} catch (InterruptedException | IOException e) {
// e.printStackTrace();
}
}).start();
if (packet.getPacket() instanceof AcknowlegdementPacket)
return;
Integer count = packetsSentTries.get(packet);
if (count == null) {
packetsSentTries.put(packet, 1);
} else {
packetsSentTries.put(packet, count + 1);
}
}
/**
* Send a packet to socket and try to resend if an acknowlegment was not recieved
* @param packet
* @param address
* @throws IOException
*/
private synchronized void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
sendPacketToSocket(packet, address);
debugSend(packet, address);
ReliablePacketAddress reliablePacketAddress = new ReliablePacketAddress(packet, address);
if (!(packet.getPacket() instanceof AcknowlegdementPacket)) {
Thread newThread = new Thread(() -> tryResend(reliablePacketAddress));
this.addressContexts.get(address).sendThreads.add(newThread);
newThread.start();
}
}
/**
* Send a packet (and encapsulate it)
* @param packet
* @param address
* @throws IOException
*/
public synchronized void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
tryAddContext(address);
ReliablePacket reliablePacket = new ReliablePacket(packet, getNextSeqSend(address));
sendPacket(reliablePacket, address);
}
/**
* Try to resend a packet
* @param reliablePacketAddress
*/
private void tryResend(ReliablePacketAddress reliablePacketAddress) {
try {
while (!Thread.interrupted()) {
Thread.sleep(RETRY_INTERVAL);
var packetsSentTries = this.addressContexts.get(reliablePacketAddress.address()).packetsSentTries;
// the packet has been received
if (!packetsSentTries.containsKey(reliablePacketAddress.packet()))
break;
Integer sendCount = packetsSentTries.get(reliablePacketAddress.packet());
if (sendCount > MAX_SEND_TRY) {
close(reliablePacketAddress.address());
debugPrint(
"Packet" + reliablePacketAddress.packet() + " not send after " + MAX_SEND_TRY + " tries");
// simulating a fake disconnect packet
this.socket.handlePacket(new DisconnectPacket("Timed out"), reliablePacketAddress.address());
break;
}
boolean client = reliablePacketAddress.address().getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Trying to resend the packet "
+ reliablePacketAddress.packet().getSeq() + " ...");
sendPacketToSocket(reliablePacketAddress.packet(), reliablePacketAddress.address());
}
} catch (InterruptedException e) {
// e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
AdressContext ctx = this.addressContexts.get(reliablePacketAddress.address());
if (ctx != null)
ctx.sendThreads.remove(Thread.currentThread());
}
/**
* @param address
* @return The smallest sequence number recieved
*/
private ReliablePacket getMinimumSeqReceived(InetSocketAddress address) {
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
if (packetRecvBuffer.isEmpty())
return null;
return Collections.min(packetRecvBuffer, (rel1, rel2) -> {
return Integer.compare(rel1.getSeq(), rel2.getSeq());
});
}
/**
* Move packets in buffer to the packet recieved queue to be processed by the app
* @param address
* @return the sequence number of the last packet that was added to the queue
*/
private int fillPacketQueue(InetSocketAddress address) {
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
ReliablePacket minimum = getMinimumSeqReceived(address);
int lastSeqProcessed = -1;
while (true) {
this.packetQueue.add(new ReliablePacketAddress(minimum, address));
packetRecvBuffer.remove(minimum);
lastSeqProcessed = minimum.getSeq();
ReliablePacket nextMinimum = getMinimumSeqReceived(address);
if (nextMinimum == null || nextMinimum.getSeq() != minimum.getSeq() + 1)
break;
minimum = nextMinimum;
}
Collections.reverse(this.packetQueue);
return lastSeqProcessed;
}
/**
* Process packet when recieved
* @param packet
* @param address
* @throws IOException
*/
public void onPacketReceived(ReliablePacket packet, InetSocketAddress address) throws IOException {
tryAddContext(address);
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
if (packet.getPacket() instanceof AcknowlegdementPacket ackPacket) {
assert (ackPacket.getAck() != -1);
for (var entry : packetsSentTries.entrySet()) {
ReliablePacket reliablePacket = entry.getKey();
if (entry.getKey().getSeq() == ackPacket.getAck()) {
packetsSentTries.remove(reliablePacket);
break;
}
}
return;
}
if (this.addressContexts.get(address).packetRecvBuffer.contains(packet)) {
debugPrint("The packet has already been received !");
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
return;
}
if (packet.getSeq() < getCurrentSeqRecv(address)) {
debugPrint("Packet too old, current : " + getCurrentSeqRecv(address));
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
return;
}
this.addressContexts.get(address).packetRecvBuffer.add(packet);
debugRecv(packet, address);
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
// got the packet in the right order
if (packet.getSeq() == getCurrentSeqRecv(address)) {
setSeqRecv(address, fillPacketQueue(address) + 1);
}
}
/**
* @return The next packet in the queue
*/
public Entry<Packet, InetSocketAddress> getNextPacket() {
if (this.packetQueue.isEmpty())
return null;
ReliablePacketAddress last = this.packetQueue.pop();
var entry = Map.entry(last.packet().getPacket(), last.address);
return entry;
}
/**
* Closes the connexion
* @param adress
*/
private void close(InetSocketAddress adress) {
var ctx = this.addressContexts.get(adress);
if (ctx != null)
close(ctx);
}
/**
* Stop the threads of the connexion
* @param adressContext
*/
private void close(AdressContext adressContext) {
for (Thread thread : adressContext.sendThreads) {
thread.interrupt();
}
}
/**
* Stop the PacketPool
*/
public void close() {
for (AdressContext adressContext : this.addressContexts.values()) {
close(adressContext);
}
}
}

View File

@@ -0,0 +1,38 @@
package network;
import java.io.Serializable;
import network.protocol.Packet;
public class ReliablePacket implements Serializable {
private final Packet packet;
private final int seq;
public ReliablePacket(Packet packet, int seq) {
this.packet = packet;
this.seq = seq;
}
/**
* @return The encapsulated packet
*/
public Packet getPacket() {
return packet;
}
/**
* @return The sequence number of this packet
*/
public int getSeq() {
return seq;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ReliablePacket packet)
return packet.getSeq() == this.getSeq();
return false;
}
}

View File

@@ -0,0 +1,158 @@
package network;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import network.protocol.Packet;
import utilities.Signal;
public class Socket {
private final DatagramSocket socket;
private final List<PacketHandler> handlers;
private final Thread readThread;
private final PacketPool packetPool;
public final Signal onClose = new Signal();
/**
* Construct a UDP Socket to connect to a server
* @throws SocketException
*/
public Socket() throws SocketException {
this.socket = new DatagramSocket();
this.handlers = new ArrayList<>();
this.packetPool = new PacketPool(this);
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
/**
* Construct a UDP Socket to listen to connexion at the specified port
* @param listeningPort the port to listen to
* @throws SocketException
*/
public Socket(int listeningPort) throws SocketException {
this.socket = new DatagramSocket(listeningPort);
this.handlers = new ArrayList<>();
this.packetPool = new PacketPool(this);
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
public void addHandler(PacketHandler handler) {
this.handlers.add(handler);
}
/**
* @return this.socket.getLocalPort()
*/
public int getLocalPort() {
return this.socket.getLocalPort();
}
// needs to be accessible by PacketPool
/**
* Send a packet to the specified address
* @param packet the packet to send
* @param address the address to send to
* @throws IOException
*/
void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(stream);
oos.writeObject(packet);
oos.flush();
byte[] data = stream.toByteArray();
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
address.getPort());
this.socket.send(dataPacket);
}
/**
* Try to recieve packets and send them to the PacketPool.
* This method blocks until something has been read
* @throws IOException
* @throws ClassNotFoundException
*/
private void recievePacketReliable()
throws IOException, ClassNotFoundException {
byte[] buffer = new byte[65535];
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(dataPacket);
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
ReliablePacket packet = (ReliablePacket) ois.readObject();
this.packetPool.onPacketReceived(packet, address);
}
/**
* Send a packet to the specified address
* @param packet the packet to send
* @param address the address to send to
* @throws IOException
*/
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
this.packetPool.sendPacket(packet, address);
}
/**
* Recieve packet in a reliable way (packets are in the right order).
* This method is blocking
* @throws IOException
* @throws ClassNotFoundException
*/
private void recievePacket() throws IOException, ClassNotFoundException {
this.recievePacketReliable();
var entry = this.packetPool.getNextPacket();
while (entry != null) {
this.handlePacket(entry.getKey(), entry.getValue());
entry = this.packetPool.getNextPacket();
}
}
/**
* Close the socket
*/
public void close() {
this.socket.close();
this.packetPool.close();
this.readThread.interrupt();
this.onClose.emit();
}
/**
* Dispatch the packet
* @param packet the packet to dispatch
* @param address the incoming address
*/
void handlePacket(Packet packet, InetSocketAddress address) {
for (PacketHandler handler : this.handlers) {
handler.handlePacket(packet, address);
}
}
/**
* Loop to read all packets
*/
private void readLoop() {
try {
while (!Thread.interrupted()) {
recievePacket();
}
} catch (IOException | ClassNotFoundException e) {
// e.printStackTrace();
}
}
}

View File

@@ -1,5 +1,7 @@
package network.protocol;
import java.util.regex.Pattern;
public class ANSIColor {
public static final String RESET = "\u001B[0m";
public static final String BLACK = "\u001B[30m";
@@ -15,4 +17,8 @@ public class ANSIColor {
.replace("&y", GREY)
.replace("&n", RESET);
}
public static String tag(String message, String chatter){
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
}
}

View File

@@ -4,6 +4,10 @@ import java.io.Serializable;
public abstract class Packet implements Serializable {
/**
* Used for double dispatch
* @param packetVisitor the visitor to dispatch to
*/
public abstract void accept(PacketVisitor packetVisitor);
}

View File

@@ -4,12 +4,17 @@ import network.protocol.packets.*;
public interface PacketVisitor {
/**
* Use double dispatch to process a packet
* @param packet the packet to process
*/
default void visit(Packet packet) {
packet.accept(this);
}
void visitPacket(ChatMessagePacket packet);
void visitPacket(CreateRoomPacket packet);
void visitPacket(DisconnectPacket packet);
void visitPacket(JoinRoomPacket packet);
void visitPacket(LeaveRoomPacket packet);
void visitPacket(LoginPacket packet);
@@ -17,5 +22,7 @@ public interface PacketVisitor {
void visitPacket(RoomListPacket packet);
void visitPacket(SendChatMessagePacket packet);
void visitPacket(ServerResponsePacket packet);
void visitPacket(HandshakePacket packet);
void visitPacket(RequestActualRoomPacket packet);
void visitPacket(ActualRoomPacket packet);
}

View File

@@ -0,0 +1,23 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class AcknowlegdementPacket extends Packet {
private final int ack;
public AcknowlegdementPacket(int ack) {
this.ack = ack;
}
@Override
public void accept(PacketVisitor packetVisitor) {
// this packet should not be handled by the app
}
public int getAck() {
return ack;
}
}

View File

@@ -0,0 +1,21 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class ActualRoomPacket extends Packet {
private final String roomName;
public ActualRoomPacket(String roomName) {
this.roomName = roomName;
}
public String getRoomName() {
return roomName;
}
@Override
public void accept(PacketVisitor packetVisitor) {
packetVisitor.visitPacket(this);
}
}

View File

@@ -0,0 +1,23 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class DisconnectPacket extends Packet {
private final String reason;
public DisconnectPacket(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
@Override
public void accept(PacketVisitor packetVisitor) {
packetVisitor.visitPacket(this);
}
}

View File

@@ -3,8 +3,8 @@ package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class HandshakePacket extends Packet {
public HandshakePacket() {
public class RequestActualRoomPacket extends Packet {
public RequestActualRoomPacket() {
}
@Override

View File

@@ -1,19 +1,19 @@
package network.protocol.packets;
import java.util.ArrayList;
import java.util.List;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class RoomListPacket extends Packet {
private final ArrayList<String> roomNames;
private final List<String> roomNames;
public RoomListPacket(ArrayList<String> roomNames) {
public RoomListPacket(List<String> roomNames) {
this.roomNames = roomNames;
}
public ArrayList<String> getRoomNames() {
public List<String> getRoomNames() {
return roomNames;
}

View File

@@ -0,0 +1,153 @@
package server;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import network.PacketHandler;
import network.Socket;
import network.protocol.ANSIColor;
import network.protocol.Packet;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.SendChatMessagePacket;
public class Server implements PacketHandler {
private final Socket serverSocket;
private final Map<InetSocketAddress, ServerConnexion> connexions;
private final Map<InetSocketAddress, Instant> connexionTimes;
private final Map<String, ArrayList<ServerConnexion>> rooms;
public Server(int port) throws SocketException {
this.serverSocket = new Socket(port);
this.serverSocket.addHandler(this);
this.connexions = new HashMap<>();
this.connexionTimes = new HashMap<>();
this.rooms = new HashMap<>();
}
public ArrayList<String> getRoomNames() {
return rooms.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public boolean isInRoom(ServerConnexion connexion) {
return getRoomName(connexion) != null;
}
public String getRoomName(ServerConnexion connexion) {
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : rooms.entrySet()) {
if (entry.getValue().contains(connexion)) {
return entry.getKey();
}
}
return null;
}
public boolean createRoom(String roomName, ServerConnexion connexion) {
if (rooms.containsKey(roomName)) {
return false;
}
rooms.put(roomName, new ArrayList<>());
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
rooms.get(roomName).add(connexion); // Add the creator to the room
return true;
}
public boolean leaveRoom(ServerConnexion connexion) {
String roomName = getRoomName(connexion);
if (roomName != null) {
rooms.get(roomName).remove(connexion);
// Remove the room if it is empty
if (rooms.get(roomName).isEmpty()) {
rooms.remove(roomName);
}
return true;
}
return false;
}
public boolean joinRoom(String roomName, ServerConnexion connexion) {
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
if (rooms.containsKey(roomName) && !rooms.get(roomName).contains(connexion)) {
rooms.get(roomName).add(connexion);
return true;
}
return false;
}
public boolean sendToRoom(String roomName, ChatMessagePacket packet) {
if (roomName != null && rooms.containsKey(roomName)) {
rooms.get(roomName).forEach(con -> con.sendPacket(
new ChatMessagePacket(packet.getTime(), packet.getChatter(), ANSIColor.tag(packet.getContent(), con.getChatterName()))
));
return true;
}
return false;
}
public boolean sendToRoom(ServerConnexion connexion, ChatMessagePacket packet) {
String roomName = getRoomName(connexion);
return sendToRoom(roomName, packet);
}
public boolean sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) {
return sendToRoom(connexion,
new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent()));
}
public void close() {
this.serverSocket.close();
}
public boolean hasChatterName(String pseudo) {
return this.connexions.values().stream()
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
}
void removeConnexion(ServerConnexion connexion) {
for (var it = this.connexions.entrySet().iterator(); it.hasNext();) {
var entry = it.next();
if (entry.getValue() == connexion) {
it.remove();
break;
}
}
for (var entry : this.rooms.entrySet()) {
if (entry.getValue().remove(connexion))
return;
}
}
@Override
public void handlePacket(Packet packet, InetSocketAddress address) {
if (!connexions.containsKey(address)) {
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
}
this.connexions.get(address).visit(packet);
}
/**
* Avoid the server to spam the chat
* @param address the address of the connexion
* @return true if the connexion is the first received or older than 5 seconds
*/
public boolean handleConnexionTime(InetSocketAddress address) {
if (!connexionTimes.containsKey(address)) {
connexionTimes.put(address, Instant.now());
return true;
}
Instant lastConnexion = connexionTimes.get(address);
if (Instant.now().isAfter(lastConnexion.plusSeconds(5))) {
connexionTimes.put(address, Instant.now());
return true;
}
return false;
}
public int getRunningPort() {
return this.serverSocket.getLocalPort();
}
}

View File

@@ -0,0 +1,206 @@
package server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.time.Instant;
import network.Socket;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.*;
import network.protocol.packets.ServerResponsePacket.Response;
public class ServerConnexion implements PacketVisitor {
private final Server server;
private final InetSocketAddress clientAddress;
private final Socket socket;
private String chatterName;
public ServerConnexion(Server server, Socket socket, InetSocketAddress clientAddress) {
this.clientAddress = clientAddress;
this.server = server;
this.socket = socket;
}
public String getChatterName() {
return this.chatterName;
}
public void sendPacket(Packet packet) {
try {
this.socket.sendPacket(packet, clientAddress);
} catch (IOException e) {
e.printStackTrace();
this.server.removeConnexion(this);
}
}
/**
* Check if the client is logged in
* @return true if the client is logged in
*/
private boolean checkLogin() {
if (this.chatterName != null && this.chatterName.isEmpty()) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return false;
}
return true;
}
/**
* Make the server create a room & enter it
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(CreateRoomPacket packet) {
if (!checkLogin())
return;
boolean created = server.createRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(created ? Response.RoomCreated : Response.RoomNotCreated));
if (created)
onRoomJoin();
}
/**
* Make the server join a room
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(JoinRoomPacket packet) {
if (!checkLogin())
return;
if (server.getRoomName(this) != null) {
server.leaveRoom(this);
return;
}
boolean joined = server.joinRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(joined ? Response.RoomJoined : Response.RoomNotJoined));
if (joined)
onRoomJoin();
}
/**
* Make the server leave a room
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(LeaveRoomPacket packet) {
if (!checkLogin())
return;
String roomName = this.server.getRoomName(this);
boolean left = server.leaveRoom(this);
sendPacket(new ServerResponsePacket(left ? Response.RoomLeft : Response.RoomNotLeft));
if (left)
onRoomLeave(roomName);
}
/**
* Checks if the pseudo is available (nobody connected with the same pseudo)
* @param packet the packet containing the pseudo
*/
@Override
public void visitPacket(LoginPacket packet) {
if (packet.getPseudo().isEmpty() || server.hasChatterName(packet.getPseudo())) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return;
}
this.chatterName = packet.getPseudo();
sendPacket(new ServerResponsePacket(Response.AuthSuccess));
sendPacket(new RoomListPacket(server.getRoomNames()));
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
}
/**
* Get the list of rooms
* @param packet the packet containing the request
*/
@Override
public void visitPacket(RequestRoomListPacket packet) {
if (!checkLogin())
return;
sendPacket(new RoomListPacket(server.getRoomNames()));
}
/**
* Send a message to the room
* @param packet the packet containing the message
*/
@Override
public void visitPacket(SendChatMessagePacket packet) {
if (!checkLogin())
return;
boolean messageSent = server.sendToRoom(this, packet);
sendPacket(new ServerResponsePacket(messageSent ? Response.MessageSent : Response.MessageNotSent));
}
/**
* Disconnect the client
* @param packet the packet containing the request
*/
@Override
public void visitPacket(DisconnectPacket packet) {
this.onDisconnect();
}
/**
* Remove all room connections while disconnecting the client
*/
private void onDisconnect() {
if (this.server.isInRoom(this)) {
this.onRoomLeave(this.server.getRoomName(this));
}
this.server.removeConnexion(this);
if (chatterName != null)
System.out.println("[Server] Chatter " + chatterName + " disconnected !");
}
/**
* Send a message to the room when a client joins
*/
private void onRoomJoin() {
String joinMessage = "Chatter " + this.chatterName + " joined the room !";
this.server.sendToRoom(this, new ChatMessagePacket(Instant.now(), "", joinMessage));
}
/**
* Send a message to the room when a client leaves
* @param roomName the name of the room
*/
private void onRoomLeave(String roomName) {
String joinMessage = "Chatter " + this.chatterName + " left the room !";
this.server.sendToRoom(roomName, new ChatMessagePacket(Instant.now(), "", joinMessage));
}
@Override
public void visitPacket(RoomListPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ChatMessagePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ServerResponsePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(RequestActualRoomPacket packet) {
sendPacket(new ActualRoomPacket(server.getRoomName(this)));
}
@Override
public void visitPacket(ActualRoomPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
}

View File

@@ -0,0 +1,37 @@
package server;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.stage.Screen;
import java.io.IOException;
import java.util.Objects;
public class ServerGui extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/server/serverVue.fxml"));
Scene scene = new Scene(loader.load(), 400, 240);
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - scene.getWidth() / 2;
double yPos = screenHeight / 2 - scene.getHeight() / 2;
scene.getStylesheets().add(getClass().getResource("serverStyle.css").toExternalForm());
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
stage.setTitle("Liscord Server");
stage.setScene(scene);
stage.setX(xPos);
stage.setY(yPos);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

View File

@@ -0,0 +1,42 @@
package server;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import network.IPAddressFinder;
import java.net.SocketException;
import java.net.UnknownHostException;
public class ServerGuiController {
@FXML
private Label IPAddress;
@FXML
private Button toggleServerButton;
private Server server;
@FXML
public void initialize() {
IPAddress.setText("IP Address: " + IPAddressFinder.findIPAddress());
}
@FXML
public void toggleServer() throws SocketException {
if(server == null){
toggleServerButton.setDisable(true);
server = new Server(6665);
toggleServerButton.setDisable(false);
toggleServerButton.setText("Stop Server");
System.out.println("Server started on port 6665");
} else {
toggleServerButton.setDisable(true);
server.close();
server = null;
toggleServerButton.setDisable(false);
toggleServerButton.setText("Start Server");
}
}
}

View File

@@ -0,0 +1,24 @@
package utilities;
import java.util.regex.Pattern;
public class ANSIColor {
public static final String RESET = "\u001B[0m";
public static final String BLACK = "\u001B[30m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String BLUE = "\u001B[34m";
public static final String GREY = "\u001B[37m";
public static String formatString(String message){
return message.replace("&r", RED)
.replace("&g", GREEN)
.replace("&b", BLUE)
.replace("&y", GREY)
.replace("&n", RESET);
}
public static String tag(String message, String chatter){
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
}
}

View File

@@ -0,0 +1,17 @@
package utilities;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class FxUtilities {
public static void centerStage(Stage stage) {
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - stage.getWidth() / 2;
double yPos = screenHeight / 2 - stage.getHeight() / 2;
stage.setX(xPos);
stage.setY(yPos);
}
}

View File

@@ -0,0 +1,38 @@
package utilities;
import java.util.HashSet;
import java.util.Set;
public class Signal {
private final Class<?>[] types;
private final Set<Slot> listeners;
public Signal(Class<?>... types) {
this.types = types;
this.listeners = new HashSet<>();
}
public void connect(Slot listener) {
this.listeners.add(listener);
}
public void clear() {
this.listeners.clear();
}
public void emit(Object... values) {
for (Slot listener : this.listeners) {
if (values.length != types.length) {
throw new UnsupportedOperationException("The number of provided arguments is not right");
}
for (int i = 0; i < values.length; i++) {
if (!types[i].isInstance(values[i])) {
throw new UnsupportedOperationException("Incorrect value at index " + i);
}
}
listener.run(values);
}
}
}

View File

@@ -0,0 +1,5 @@
package utilities;
public interface Slot {
void run(Object ... args);
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.control.Label?>
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientLoading" style="-fx-background-color: lightgrey;">
<Arc fx:id="spinnerArc" centerX="50" centerY="50" radiusX="30" radiusY="30"
startAngle="30" length="330" fill="transparent" stroke="blue" strokeWidth="5" onMouseClicked="#login"
/>
</StackPane>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<VBox alignment="CENTER" spacing="10.0"
xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientLogin"
prefHeight="400.0" prefWidth="600.0"
styleClass="login-box">
<padding>
<Insets bottom="10.0" left="100.0" right="100.0" top="10.0"/>
</padding>
<Label text="Enter your username"/>
<TextField fx:id="usernameField" onAction="#login" styleClass="login"/>
<Button text="Login" onAction="#login"/>
</VBox>

View File

@@ -0,0 +1,17 @@
.login-box, .logo-box, .rooms-list, .chat-list {
-fx-background-color: lightgray;
}
.login {
-fx-border-color: transparent;
}
.login:focused {
-fx-border-color: lightgrey;
}
.rooms-label {
-fx-font-size: 18px;
-fx-padding: 10px;
-fx-background-color: lightgray;
}

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientGuiController"
prefHeight="400.0" prefWidth="600.0" styleClass="vue-container" fx:id="vueContainer">
<top>
<HBox alignment="CENTER" styleClass="logo-box">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<ImageView fitHeight="50.0" fitWidth="50.0">
<Image url="@/liscord.png" />
</ImageView>
<Label text="Liscord" style="-fx-font-size: 24px; -fx-font-weight: bold; -fx-padding: 0 0 0 10px"/>
</HBox>
</top>
<left>
<ScrollPane prefWidth="200.0" styleClass="rooms-pane" fx:id="roomsPane" fitToWidth="true">
<VBox fx:id="rooms" spacing="10.0" alignment="CENTER" VBox.vgrow="ALWAYS">
<Label text="Rooms" styleClass="rooms-label"/>
<VBox fx:id="roomList" styleClass="rooms-list" spacing="5.0" VBox.vgrow="ALWAYS" alignment="CENTER"/>
</VBox>
</ScrollPane>
</left>
<center>
<BorderPane>
<center>
<ScrollPane styleClass="chat-pane" fx:id="chatPane" fitToWidth="true">
<VBox fx:id="chat" spacing="10.0">
<VBox fx:id="chatList" styleClass="chat-list" spacing="5.0" VBox.vgrow="ALWAYS"/>
</VBox>
</ScrollPane>
</center>
<bottom>
<HBox fx:id="chatInput" styleClass="chat-input" spacing="5.0" style="-fx-padding: 10;" />
</bottom>
</BorderPane>
</center>
</BorderPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,27 @@
.root{
-fx-background-color: darkblue;
-fx-text-fill: blue;
-fx-font-weight: bold;
}
.address {
-fx-text-fill: white;
-fx-font-size: 25px;
}
.toggle-button {
-fx-background-color: darkblue;
-fx-text-fill: white;
-fx-font-size: 20px;
-fx-padding: 10px;
-fx-border-width: 2px;
-fx-border-color: white;
-fx-cursor: hand;
-fx-border-radius: 5;
}
.toggle-button:hover {
-fx-background-color: white;
-fx-text-fill: darkblue;
transition: -fx-background-color 0.3s, -fx-text-fill 0.3s;
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox alignment="CENTER" spacing="20.0" xmlns="http://javafx.com/javafx/11.0.14-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="server.ServerGuiController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<Label fx:id="IPAddress" styleClass="address" />
<Button text="Start Server" onAction="#toggleServer" fx:id="toggleServerButton" styleClass="toggle-button"/>
</VBox>

View File

@@ -0,0 +1,6 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=false
org.gradle.console=plain
org.gradle.logging.level=quiet

View File

@@ -0,0 +1,10 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
guava = "33.3.1-jre"
junit-jupiter = "5.11.3"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
ChatApp/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
ChatApp/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

14
ChatApp/settings.gradle Normal file
View File

@@ -0,0 +1,14 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.13/userguide/multi_project_builds.html in the Gradle documentation.
*/
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
}
rootProject.name = 'ChatApp'
include('app')

View File

@@ -1,24 +0,0 @@
import java.net.InetSocketAddress;
import java.util.Scanner;
import client.Client;
import server.Server;
public class ChatApp {
public static void main(String[] args) throws Exception {
int port = 6665;
Server server = new Server(port);
Client client = new Client(new InetSocketAddress("localhost", port));
client.SendCreateRoom("101");
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
System.out.print("\033[1A");
System.out.print("\r\033[2K");
System.out.flush();
client.visitMessage(message);
}
}
}

View File

@@ -1,142 +0,0 @@
package client;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Objects;
import java.util.Scanner;
import network.protocol.ANSIColor;
import network.protocol.packets.*;
public class Client {
private final ClientConnexion connexion;
public static void main(String[] args) {
String host = "localhost";
int port = 6665;
try {
Client client = new Client(new InetSocketAddress(host, port));
Scanner scanner = new Scanner(System.in);
while(true) {
String message = scanner.nextLine();
client.visitMessage(message);
}
} catch (SocketException e) {
e.printStackTrace();
}
}
public Client(InetSocketAddress serverAddress) throws SocketException {
this.connexion = new ClientConnexion(new DatagramSocket(), serverAddress);
int tries = 0;
try {
connexion.sendPacket(new HandshakePacket());
} catch (IOException e) {
e.printStackTrace();
}
while(!connexion.connected) {
try {
Thread.sleep(100);
if(tries++ > 5) {
System.out.println("Server is not responding");
System.exit(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Scanner scanner = new Scanner(System.in);
System.out.println("Enter your pseudo:");
String pseudo = scanner.nextLine();
login(pseudo);
}
public void visitMessage(String message){
try {
if(message.startsWith("/")){
if(message.startsWith("/createRoom ")) {
String roomName = message.substring(12).trim();
SendCreateRoom(roomName);
} else if(message.startsWith("/create ")){
String roomName = message.substring(8).trim();
SendCreateRoom(roomName);
} else if(message.equals("/listRooms") || message.equals("/list")) {
RequestRoomList();
} else if(message.startsWith("/joinRoom ")) {
String roomName = message.substring(10).trim();
SendJoinRoom(roomName);
} else if(message.startsWith("/join ")){
String roomName = message.substring(6).trim();
SendJoinRoom(roomName);
} else if(message.equals("/leaveRoom") || message.equals("/leave")) {
SendLeaveRoom();
} else if(message.equals("/help")) {
System.out.println("Available commands:");
System.out.println("\t/createRoom <roomName>");
System.out.println("\t/listRooms");
System.out.println("\t/joinRoom <roomName>");
System.out.println("\t/leaveRoom");
System.out.println("\t/help");
}
else {
System.out.println(ANSIColor.formatString("&rUnknown command&n"));
}
} else {
SendChatMessage(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void login(String pseudo) {
try {
this.connexion.sendPacket(new LoginPacket(pseudo));
} catch (IOException e) {
e.printStackTrace();
}
}
public void SendChatMessage(String message) {
try {
this.connexion.sendPacket(new SendChatMessagePacket(message));
} catch (IOException e) {
e.printStackTrace();
}
}
public void SendCreateRoom(String roomName) {
try {
this.connexion.sendPacket(new CreateRoomPacket(roomName));
} catch (Exception e) {
e.printStackTrace();
}
}
public void SendJoinRoom(String roomName) {
try {
this.connexion.sendPacket(new JoinRoomPacket(roomName));
} catch (Exception e) {
e.printStackTrace();
}
}
public void SendLeaveRoom() {
try {
this.connexion.sendPacket(new LeaveRoomPacket());
} catch (Exception e) {
e.printStackTrace();
}
}
public void RequestRoomList() {
try {
this.connexion.sendPacket(new RequestRoomListPacket());
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -1,48 +0,0 @@
package network;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.protocol.Packet;
public class SocketReader {
private final DatagramSocket socket;
private final Thread readThread;
private final PacketHandler handler;
public SocketReader(DatagramSocket socket, PacketHandler handler) {
this.socket = socket;
this.handler = handler;
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
public void stop() {
this.readThread.interrupt();
}
private void readLoop() {
while (!Thread.interrupted()) {
try {
byte[] buffer = new byte[65535];
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(dataPacket);
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
Packet packet = (Packet) ois.readObject();
this.handler.handlePacket(packet, address);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -1,30 +0,0 @@
package network;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.protocol.Packet;
public class SocketWriter {
private final DatagramSocket socket;
public SocketWriter(DatagramSocket socket) {
this.socket = socket;
}
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(stream);
oos.writeObject(packet);
oos.flush();
byte[] data = stream.toByteArray();
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
address.getPort());
this.socket.send(dataPacket);
}
}

View File

@@ -1,100 +0,0 @@
package server;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import network.PacketHandler;
import network.SocketReader;
import network.protocol.Packet;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.SendChatMessagePacket;
public class Server implements PacketHandler {
private final DatagramSocket serverSocket;
private final Map<InetSocketAddress, ServerConnexion> connexions;
private final SocketReader reader;
private final Map<String, ArrayList<ServerConnexion>> roomNames;
public Server(int port) throws SocketException {
this.serverSocket = new DatagramSocket(port);
this.connexions = new HashMap<>();
this.reader = new SocketReader(serverSocket, this);
this.roomNames = new HashMap<>();
}
public ArrayList<String> getRoomNames() {
return roomNames.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public String getRoomName(ServerConnexion connexion) {
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : roomNames.entrySet()) {
if(entry.getValue().contains(connexion)) {
return entry.getKey();
}
}
return null;
}
public void createRoom(String roomName, ServerConnexion connexion) throws SocketException {
if(roomNames.containsKey(roomName)) {
throw new SocketException("Room already exists");
}
roomNames.put(roomName, new ArrayList<>());
roomNames.get(roomName).add(connexion);
}
public void leaveRoom(ServerConnexion connexion) throws SocketException {
String roomName = getRoomName(connexion);
if(roomName != null) {
roomNames.get(roomName).remove(connexion);
// Remove the room if it is empty
if(roomNames.get(roomName).isEmpty()) {
roomNames.remove(roomName);
}
return;
}
throw new SocketException("Room does not exist");
}
public void joinRoom(String roomName, ServerConnexion connexion) throws SocketException {
if(roomNames.containsKey(roomName) && !roomNames.get(roomName).contains(connexion)) {
roomNames.get(roomName).add(connexion);
return;
}
throw new SocketException("Room does not exist");
}
public void sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) throws SocketException {
String roomName = getRoomName(connexion);
ChatMessagePacket chatPacket = new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent());
if(roomName != null && roomNames.containsKey(roomName)) {
roomNames.get(roomName).forEach(con -> con.sendPacket(chatPacket));
return;
}
throw new SocketException("You are not in a room or the room does not exist");
}
public void close() {
this.reader.stop();
}
public boolean hasChatterName(String pseudo) {
return this.connexions.values().stream()
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
}
@Override
public void handlePacket(Packet packet, InetSocketAddress address) {
if (!connexions.containsKey(address)) {
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
}
this.connexions.get(address).visit(packet);
}
}

View File

@@ -1,119 +0,0 @@
package server;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import network.SocketWriter;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.*;
import network.protocol.packets.ServerResponsePacket.Response;
public class ServerConnexion implements PacketVisitor {
private final Server server;
private final InetSocketAddress clientAddress;
private final SocketWriter writer;
private String chatterName;
public ServerConnexion(Server server, DatagramSocket socket, InetSocketAddress clientAddress) {
this.clientAddress = clientAddress;
this.server = server;
this.writer = new SocketWriter(socket);
}
public String getChatterName() {
return this.chatterName;
}
public void sendPacket(Packet packet) {
try {
this.writer.sendPacket(packet, clientAddress);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void visitPacket(ChatMessagePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(CreateRoomPacket packet) {
try {
server.createRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(Response.RoomCreated));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotCreated));
}
}
@Override
public void visitPacket(JoinRoomPacket packet) {
try {
server.joinRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(Response.RoomJoined));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotJoined));
}
}
@Override
public void visitPacket(LeaveRoomPacket packet) {
try {
server.leaveRoom(this);
sendPacket(new ServerResponsePacket(Response.RoomLeft));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotLeft));
}
}
@Override
public void visitPacket(LoginPacket packet) {
if (server.hasChatterName(packet.getPseudo())) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return;
}
this.chatterName = packet.getPseudo();
sendPacket(new RoomListPacket(server.getRoomNames()));
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
}
@Override
public void visitPacket(RequestRoomListPacket packet) {
sendPacket(new RoomListPacket(server.getRoomNames()));
}
@Override
public void visitPacket(RoomListPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(SendChatMessagePacket packet) {
try {
server.sendToRoom(this, packet);
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.MessageNotSent));
}
}
@Override
public void visitPacket(ServerResponsePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(HandshakePacket packet) {
System.out.println("[Server] Handshake received from " + clientAddress);
sendPacket(new HandshakePacket());
}
}

2
Sudoku

Submodule Sudoku updated: af0ac0ff77...05df8a56a7