From c88b6ac234c87f9f424da88dc84b9e9e28ca24f0 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 16 Nov 2024 16:44:29 +0200 Subject: [PATCH] complete lab3 --- Lab3/.vscode/launch.json | 38 +++ Lab3/.vscode/tasks.json | 41 +++ Lab3/ChatRoom/ChatRoom.csproj | 20 ++ Lab3/ChatRoom/ChatRoomLogic.cs | 296 ++++++++++++++++++ Lab3/ChatRoom/ChatRoomService.cs | 156 +++++++++ Lab3/ChatRoom/ChatRoomState.cs | 97 ++++++ Lab3/ChatRoom/Server.cs | 73 +++++ Lab3/ChatRoomContract/ChatRoomClient.cs | 241 ++++++++++++++ Lab3/ChatRoomContract/ChatRoomContract.csproj | 14 + Lab3/ChatRoomContract/Config.cs | 30 ++ Lab3/ChatRoomContract/IChatRoomService.cs | 81 +++++ Lab3/ChatRoomContract/RPCMessage.cs | 49 +++ Lab3/Lab3.sln | 43 +++ Lab3/Moderator/Moderator.cs | 102 ++++++ Lab3/Moderator/Moderator.csproj | 19 ++ Lab3/Participant/Participant.cs | 107 +++++++ Lab3/Participant/Participant.csproj | 19 ++ 17 files changed, 1426 insertions(+) create mode 100644 Lab3/.vscode/launch.json create mode 100644 Lab3/.vscode/tasks.json create mode 100644 Lab3/ChatRoom/ChatRoom.csproj create mode 100644 Lab3/ChatRoom/ChatRoomLogic.cs create mode 100644 Lab3/ChatRoom/ChatRoomService.cs create mode 100644 Lab3/ChatRoom/ChatRoomState.cs create mode 100644 Lab3/ChatRoom/Server.cs create mode 100644 Lab3/ChatRoomContract/ChatRoomClient.cs create mode 100644 Lab3/ChatRoomContract/ChatRoomContract.csproj create mode 100644 Lab3/ChatRoomContract/Config.cs create mode 100644 Lab3/ChatRoomContract/IChatRoomService.cs create mode 100644 Lab3/ChatRoomContract/RPCMessage.cs create mode 100644 Lab3/Lab3.sln create mode 100644 Lab3/Moderator/Moderator.cs create mode 100644 Lab3/Moderator/Moderator.csproj create mode 100644 Lab3/Participant/Participant.cs create mode 100644 Lab3/Participant/Participant.csproj diff --git a/Lab3/.vscode/launch.json b/Lab3/.vscode/launch.json new file mode 100644 index 0000000..54b90be --- /dev/null +++ b/Lab3/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ChatRoom", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ChatRoom", + "program": "${workspaceFolder}/ChatRoom/bin/Debug/net6.0/ChatRoom.dll", + "args": [], + "cwd": "${workspaceFolder}/ChatRoom", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Moderator", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Moderator", + "program": "${workspaceFolder}/Moderator/bin/Debug/net6.0/Moderator.dll", + "args": [], + "cwd": "${workspaceFolder}/Moderator", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Participant", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Participant", + "program": "${workspaceFolder}/Participant/bin/Debug/net6.0/Participant.dll", + "args": [], + "cwd": "${workspaceFolder}/Participant", + "console": "externalTerminal", + "stopAtEntry": false + } + ] +} \ No newline at end of file diff --git a/Lab3/.vscode/tasks.json b/Lab3/.vscode/tasks.json new file mode 100644 index 0000000..cf312c1 --- /dev/null +++ b/Lab3/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-ChatRoom", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ChatRoom/ChatRoom.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Moderator", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Moderator/Moderator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Participant", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Participant/Participant.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Lab3/ChatRoom/ChatRoom.csproj b/Lab3/ChatRoom/ChatRoom.csproj new file mode 100644 index 0000000..30cd58d --- /dev/null +++ b/Lab3/ChatRoom/ChatRoom.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/Lab3/ChatRoom/ChatRoomLogic.cs b/Lab3/ChatRoom/ChatRoomLogic.cs new file mode 100644 index 0000000..7d58b0f --- /dev/null +++ b/Lab3/ChatRoom/ChatRoomLogic.cs @@ -0,0 +1,296 @@ +using NLog; + +namespace ChatRoom; + +/// +/// Chat room service logic +/// +public class ChatRoomLogic +{ + /// + /// Background thread of deleting approved or rejected messages + /// + private Thread thread; + /// + /// Chat Room state + /// + private ChatRoomState state = new ChatRoomState(); + /// + /// Logger + /// + private Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Chat room logic constructor + /// + public ChatRoomLogic() + { + thread = new Thread(BackgroundTask); + thread.Start(); + } + + /// + /// Generate the next incrementing ID + /// + /// Unique ID + int NextId() + { + int id = state.lastUniqueId; + state.lastUniqueId++; + return id; + } + + /// + /// Register a client + /// + /// Client name + /// Client ID + public int RegisterClient(string name) + { + lock (state.accessLock) + { + int clientId = NextId(); + state.clients.Add(new Client + { + id = clientId, + name = name + }); + log.Info($"Registered with client '{name}' with id {clientId}"); + return clientId; + } + } + + /// + /// Find a client by ID, can return NULL if not found + /// + /// Client ID + /// Optional client object + Client? FindClientById(int clientId) + { + foreach (var client in state.clients) + { + if (client.id == clientId) + { + return client; + } + } + return null; + } + + /// + /// Find message by ID, can return NULL if not found + /// + /// Message ID + /// Optional message object + Message? FindMessageById(int messageId) + { + foreach (var message in state.messages) + { + if (message.id == messageId) + { + return message; + } + } + return null; + } + + /// + /// Check if client is still blocked, will clear `blockedUntil` if client became unblocked + /// + /// Client object + /// Is client blocked? + bool GetAndUpdateBlockedState(Client client) + { + if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil) + { + client.blockedUntil = null; + } + + return client.blockedUntil != null; + } + + /// + /// Send a message + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending the message successful, can fail if client is blocked + public bool SendMessage(int clientId, string contents, bool needsToBeCensored) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return false; + } + + if (GetAndUpdateBlockedState(client)) + { + return false; + } + + var message = new Message + { + id = NextId(), + clientId = clientId, + contents = contents, + needsToBeCensored = needsToBeCensored, + status = MessageStatus.WaitingForModerator, + sentAt = DateTime.UtcNow + }; + state.messages.Add(message); + log.Info($"Client '{client.name}' ({client.id}) sent message '{contents}' ({message.id}). Needs to censored: {needsToBeCensored}"); + } + + return true; + } + + /// + /// Get next message which isin't approved or rejected + /// + /// Optional message object + public ChatRoomContract.Message? GetNewMessage() + { + lock (state.accessLock) + { + foreach (var message in state.messages) + { + if (message.status != MessageStatus.WaitingForModerator) continue; + + log.Info($"Message '{message.id}' given to moderator"); + message.status = MessageStatus.GivenToModerator; + return new ChatRoomContract.Message + { + id = message.id, + contents = message.contents, + needsToBeCensored = message.needsToBeCensored + }; + } + } + + return null; + } + + /// + /// Approve a message + /// + /// Message ID + public void ApproveMessage(int messageId) + { + lock (state.accessLock) + { + var message = FindMessageById(messageId); + if (message == null) + { + return; + } + + if (message.status != MessageStatus.GivenToModerator) + { + return; + } + + message.status = MessageStatus.Approved; + log.Info($"Message {message.id} was approved"); + } + } + + /// + /// Reject a message + /// + /// Message ID + public void RejectMessage(int messageId) + { + lock (state.accessLock) + { + var message = FindMessageById(messageId); + if (message == null) + { + return; + } + + if (message.status != MessageStatus.GivenToModerator) + { + return; + } + + message.status = MessageStatus.Rejected; + log.Info($"Message {message.id} was rejected"); + + var client = FindClientById(message.clientId); + if (client != null && !GetAndUpdateBlockedState(client)) + { + client.strikes++; + + var rnd = new Random(); + if (client.strikes > rnd.Next(0, 10)) + { + log.Info($"Client '{client.name}' ({client.id}) was blocked for {client.strikes}s"); + client.blockedUntil = DateTime.UtcNow.AddSeconds(client.strikes); + client.strikes = 0; + } + } + } + } + + /// + /// Get number of strikes a client has + /// + /// Client ID + /// Number of strikes + public int GetStrikes(int clientId) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return 0; + } + + return client.strikes; + } + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + public DateTime? GetBlockedUntil(int clientId) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return null; + } + + return client.blockedUntil; + } + } + + /// + /// Main loop for background thread. Used for deleting approved or rejected messages. + /// + public void BackgroundTask() + { + while (true) + { + lock (state.accessLock) + { + var now = DateTime.UtcNow; + int count = state.messages.RemoveAll(msg => + (now - msg.sentAt).TotalSeconds > 5 && + (msg.status == MessageStatus.Approved || msg.status == MessageStatus.Rejected) + ); + log.Info($"Running periodic cleanup, removed {count} messages"); + } + + Thread.Sleep(10 * 1000); + } + } +} diff --git a/Lab3/ChatRoom/ChatRoomService.cs b/Lab3/ChatRoom/ChatRoomService.cs new file mode 100644 index 0000000..a9b5af2 --- /dev/null +++ b/Lab3/ChatRoom/ChatRoomService.cs @@ -0,0 +1,156 @@ +using RabbitMQ.Client; +using Newtonsoft.Json; +using RabbitMQ.Client.Events; +using ChatRoomContract; +using System.Text; +using MessagePack; +using System.Diagnostics; +using NLog.LayoutRenderers.Wrappers; +using NLog; + +namespace ChatRoom; + +internal class ChatRoomService +{ + /// + /// Logger for this class. + /// + private Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Communications channel to RabbitMQ message broker. + /// + private IModel rmqChannel; + + /// + /// Service logic. + /// + private ChatRoomLogic logic = new ChatRoomLogic(); + + public ChatRoomService(IConnection connection, string exchangeName, string serverQueueName) + { + //get channel, configure exchanges and request queue + rmqChannel = connection.CreateModel(); + + rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct); + rmqChannel.QueueDeclare(queue: serverQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null); + rmqChannel.QueueBind(queue: serverQueueName, exchange: exchangeName, routingKey: serverQueueName, arguments: null); + + //connect to the queue as consumer + //XXX: see https://www.rabbitmq.com/dotnet-api-guide.html#concurrency for threading issues + var rmqConsumer = new EventingBasicConsumer(rmqChannel); + rmqConsumer.Received += (consumer, delivery) => OnMessageReceived(((EventingBasicConsumer)consumer).Model, delivery); + rmqChannel.BasicConsume(queue: serverQueueName, autoAck: true, consumer: rmqConsumer); + } + + /// + /// Is invoked to process messages received. + /// + /// Related communications channel. + /// Message deliver data. + private void OnMessageReceived(IModel channel, BasicDeliverEventArgs msgIn) + { + try + { + var msg = MessagePackSerializer.Deserialize(msgIn.Body); + Debug.Assert(msg != null); + + if (msg.isResponse == true) + { + return; + } + + bool hasResponse = false; + byte[]? response = null; + + switch (msg.method) + { + case nameof(IChatRoomService.RegisterClient): + { + var name = MessagePackSerializer.Deserialize(msg.args); + var clientId = logic.RegisterClient(name); + response = MessagePackSerializer.Serialize(clientId); + hasResponse = true; + break; + } + + case nameof(IChatRoomService.GetStrikes): + { + var clientId = MessagePackSerializer.Deserialize(msg.args); + var strikes = logic.GetStrikes(clientId); + response = MessagePackSerializer.Serialize(strikes); + hasResponse = true; + break; + } + + case nameof(IChatRoomService.GetBlockedUntil): + { + var clientId = MessagePackSerializer.Deserialize(msg.args); + var blockedUntil = logic.GetBlockedUntil(clientId); + response = MessagePackSerializer.Serialize(blockedUntil); + hasResponse = true; + break; + } + + case nameof(IChatRoomService.SendMessage): + { + var args = MessagePackSerializer.Deserialize(msg.args); + var success = logic.SendMessage(args.clientId, args.contents, args.needsToBeCensored); + response = MessagePackSerializer.Serialize(success); + hasResponse = true; + break; + } + + case nameof(IChatRoomService.GetNewMessage): + { + var newMessage = logic.GetNewMessage(); + response = MessagePackSerializer.Serialize(newMessage); + hasResponse = true; + break; + } + + case nameof(IChatRoomService.RejectMessage): + { + var messageId = MessagePackSerializer.Deserialize(msg.args); + logic.ApproveMessage(messageId); + break; + } + + case nameof(IChatRoomService.ApproveMessage): + { + var messageId = MessagePackSerializer.Deserialize(msg.args); + logic.ApproveMessage(messageId); + break; + } + + default: + { + throw new Exception("Unknown RPC method"); + } + } + + if (hasResponse) + { + var responseMsg = new RPCMessage + { + isResponse = true, + method = msg.method, + args = response + }; + + var properties = channel.CreateBasicProperties(); + properties.CorrelationId = msgIn.BasicProperties.CorrelationId; + + channel.BasicPublish( + exchange: msgIn.Exchange, + routingKey: msgIn.BasicProperties.ReplyTo, + basicProperties: properties, + body: MessagePackSerializer.Serialize(responseMsg) + ); + } + } catch (Exception e) + { + log.Error(e, "Unhandled exception caught when processing a message. The message is now lost."); + } + } +} diff --git a/Lab3/ChatRoom/ChatRoomState.cs b/Lab3/ChatRoom/ChatRoomState.cs new file mode 100644 index 0000000..f6616ea --- /dev/null +++ b/Lab3/ChatRoom/ChatRoomState.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ChatRoom; + +/// +/// Client information +/// +public class Client +{ + /// + /// Client ID + /// + public int id; + /// + /// Client name, can be the same between multiple clients + /// + public string name; + /// + /// Number of strikes + /// + public int strikes = 0; + /// + /// Until when is this client blocked from sending messages + /// + public DateTime? blockedUntil = null; +} + +/// +/// Describes the messages status/stage +/// +public enum MessageStatus +{ + WaitingForModerator, + GivenToModerator, + Approved, + Rejected +} + +/// +/// Message information +/// +public class Message +{ + /// + /// Message ID + /// + public int id; + /// + /// Client ID + /// + public int clientId; + /// + /// Message contents + /// + public string contents; + /// + /// Does this message need to be censored? + /// + public bool needsToBeCensored; + /// + /// Message status/stage + /// + public MessageStatus status = MessageStatus.WaitingForModerator; + + /// + /// When was this message sent + /// + public DateTime sentAt; +} + +public class ChatRoomState +{ + /// + /// Access lock. + /// + public readonly object accessLock = new object(); + + /// + /// Last unique ID value generated. + /// + public int lastUniqueId; + + /// + /// List of all registered clients + /// + public List clients = new List(); + + /// + /// List of messages + /// + public List messages = new List(); +} + diff --git a/Lab3/ChatRoom/Server.cs b/Lab3/ChatRoom/Server.cs new file mode 100644 index 0000000..779e68b --- /dev/null +++ b/Lab3/ChatRoom/Server.cs @@ -0,0 +1,73 @@ +using ChatRoomContract; +using NLog; + +namespace ChatRoom; + +internal class Server +{ + /// + /// Logger for this class. + /// + private Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configure loggin subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + /// + /// Program body. + /// + private void Run() + { + //configure logging + ConfigureLogging(); + + while (true) + { + try + { + var service = new ChatRoomService(Config.CreateConnection(), Config.ExchangeName, Config.ServerQueueName); + + log.Info("Server has been started."); + + //hang main thread + while (true) + { + Thread.Sleep(1000); + } + } + catch (Exception e) + { + //log exception + log.Error(e, "Unhandled exception caught. Server will now restart."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + /// + /// Program entry point. + /// + /// Command line arguments. + static void Main(string[] args) + { + var self = new Server(); + self.Run(); + } +} diff --git a/Lab3/ChatRoomContract/ChatRoomClient.cs b/Lab3/ChatRoomContract/ChatRoomClient.cs new file mode 100644 index 0000000..f2af0e9 --- /dev/null +++ b/Lab3/ChatRoomContract/ChatRoomClient.cs @@ -0,0 +1,241 @@ +using MessagePack; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace ChatRoomContract; + +/// +/// RabbitMQ chat room client +/// +public class ChatRoomClient : IChatRoomService +{ + /// + /// Exchange name + /// + private string exchangeName; + /// + /// Client queue name + /// + private string clientQueueName; + /// + /// Server queue name + /// + private string serverQueueName; + /// + /// RabbitMQ channel + /// + private IModel rmqChannel; + + /// + /// Chat Room client constructor + /// + /// RabbitMQ connection + /// Exchange name + /// Client queue name + /// Server queue name + public ChatRoomClient(IConnection connection, string exchangeName, string clientQueueName, string serverQueueName) + { + this.exchangeName = exchangeName; + this.clientQueueName = clientQueueName; + this.serverQueueName = serverQueueName; + rmqChannel = connection.CreateModel(); + + rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct); + rmqChannel.QueueDeclare(queue: clientQueueName, durable: false, exclusive: true, autoDelete: false, arguments: null); + rmqChannel.QueueBind(queue: clientQueueName, exchange: exchangeName, routingKey: clientQueueName, arguments: null); + } + + /// + /// Send rpc message without waiting for a response + /// + /// Method name + /// Serialized arguments + /// Optional correlation ID + private void CallVoid(string method, byte[] args, string? correlationId = null) + { + correlationId ??= Guid.NewGuid().ToString(); + + var requestProps = rmqChannel.CreateBasicProperties(); + requestProps.CorrelationId = correlationId; + requestProps.ReplyTo = clientQueueName; + + var msg = new RPCMessage{ + isResponse = false, + method = method, + args = args + }; + + rmqChannel.BasicPublish( + exchange: exchangeName, + routingKey: serverQueueName, + basicProperties: requestProps, + body: MessagePackSerializer.Serialize(msg) + ); + } + + /// + /// Send a rpc message and wait for a response + /// + /// Result type + /// Method name + /// Serialized arguments + /// Result + private ResultType Call(string method, byte[]? args) + { + var correlationId = Guid.NewGuid().ToString(); + var isResultReady = false; + var resultReadySignal = new AutoResetEvent(false); + + ResultType result = default; + + //ensure contents of variables set in main thread, are loadable by receiver thread + Thread.MemoryBarrier(); + + //create response message consumer + var consumer = new EventingBasicConsumer(rmqChannel); + consumer.Received += + (channel, delivery) => { + //ensure contents of variables set in main thread are loaded into this thread + Thread.MemoryBarrier(); + + if (isResultReady) + { + return; + } + + if (delivery.BasicProperties.CorrelationId != correlationId) + { + return; + } + + + var msg = MessagePackSerializer.Deserialize(delivery.Body); + if (msg.isResponse && msg.method == method) + { + if (msg.args != null) + { + result = MessagePackSerializer.Deserialize(msg.args); + } + + //indicate result has been received, ensure it is loadable by main thread + isResultReady = true; + Thread.MemoryBarrier(); + + //signal main thread that result has been received + resultReadySignal.Set(); + } + }; + + //attach message consumer to the response queue + var consumerTag = rmqChannel.BasicConsume(clientQueueName, true, consumer); + + CallVoid(method, args, correlationId); + + //wait for the result to be ready + resultReadySignal.WaitOne(); + + //ensure contents of variables set by the receiver are loaded into this thread + Thread.MemoryBarrier(); + + //detach message consumer from the response queue + rmqChannel.BasicCancel(consumerTag); + + return result; + } + + /// + /// Approve a message + /// + /// Message ID + public void ApproveMessage(int messageId) + { + CallVoid( + nameof(IChatRoomService.ApproveMessage), + MessagePackSerializer.Serialize(messageId) + ); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + public DateTime? GetBlockedUntil(int clientId) + { + return Call( + nameof(IChatRoomService.GetBlockedUntil), + MessagePackSerializer.Serialize(clientId) + ); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + public Message? GetNewMessage() + { + return Call( + nameof(IChatRoomService.GetNewMessage), + null + ); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + public int GetStrikes(int clientId) + { + return Call( + nameof(IChatRoomService.GetStrikes), + MessagePackSerializer.Serialize(clientId) + ); + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + public int RegisterClient(string name) + { + return Call( + nameof(IChatRoomService.RegisterClient), + MessagePackSerializer.Serialize(name) + ); + } + + /// + /// Reject a message + /// + /// Message ID + public void RejectMessage(int messageId) + { + CallVoid( + nameof(IChatRoomService.RejectMessage), + MessagePackSerializer.Serialize(messageId) + ); + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending successful, can fail if user is blocked + public bool SendMessage(int clientId, string contents, bool needsToBeCensored) + { + var args = new SendMessageArgs { + clientId = clientId, + contents = contents, + needsToBeCensored = needsToBeCensored + }; + + return Call( + nameof(IChatRoomService.SendMessage), + MessagePackSerializer.Serialize(args) + ); + } +} diff --git a/Lab3/ChatRoomContract/ChatRoomContract.csproj b/Lab3/ChatRoomContract/ChatRoomContract.csproj new file mode 100644 index 0000000..cabe6ba --- /dev/null +++ b/Lab3/ChatRoomContract/ChatRoomContract.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/Lab3/ChatRoomContract/Config.cs b/Lab3/ChatRoomContract/Config.cs new file mode 100644 index 0000000..4aeb1e2 --- /dev/null +++ b/Lab3/ChatRoomContract/Config.cs @@ -0,0 +1,30 @@ +using RabbitMQ.Client; + +namespace ChatRoomContract; + +public static class Config +{ + public static string ExchangeName = "T120B180.ChatRoom.Exchange"; + public static string ServerQueueName = "T120B180.ChatRoom.Server"; + public static string ClientQueueNamePrefix = "T120B180.ChatRoom.Client_"; + public static string HostName = "localhost"; + public static string Username = "guest"; + public static string Password = "guest"; + + public static string CreateClientQueueName() + { + var ClientId = Guid.NewGuid().ToString(); + return ClientQueueNamePrefix + ClientId; + } + + public static IConnection CreateConnection() + { + var rmqConnectionFactory = new ConnectionFactory + { + HostName = HostName, + UserName = Username, + Password = Password + }; + return rmqConnectionFactory.CreateConnection(); + } +} diff --git a/Lab3/ChatRoomContract/IChatRoomService.cs b/Lab3/ChatRoomContract/IChatRoomService.cs new file mode 100644 index 0000000..fbf96ed --- /dev/null +++ b/Lab3/ChatRoomContract/IChatRoomService.cs @@ -0,0 +1,81 @@ +using MessagePack; + +namespace ChatRoomContract; + +/// +/// Minimal message description +/// +[MessagePackObject] +public class Message +{ + /// + /// Message ID + /// + [Key(0)] + public int id; + /// + /// Message contents + /// + [Key(1)] + public string contents; + /// + /// Does this message need to be censored? + /// + [Key(2)] + public bool needsToBeCensored; +} + +/// +/// Chat room service contract +/// +public interface IChatRoomService +{ + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + int RegisterClient(string name); + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + int GetStrikes(int clientId); + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + DateTime? GetBlockedUntil(int clientId); + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending successful, can fail if user is blocked + bool SendMessage(int clientId, string contents, bool needsToBeCensored); + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + Message? GetNewMessage(); + + /// + /// Reject a message + /// + /// Message ID + void RejectMessage(int messageId); + + /// + /// Approve a message + /// + /// Message ID + void ApproveMessage(int messageId); +} + diff --git a/Lab3/ChatRoomContract/RPCMessage.cs b/Lab3/ChatRoomContract/RPCMessage.cs new file mode 100644 index 0000000..2acc5b1 --- /dev/null +++ b/Lab3/ChatRoomContract/RPCMessage.cs @@ -0,0 +1,49 @@ +using MessagePack; + +namespace ChatRoomContract; + +/// +/// RabbitMQ message +/// +[MessagePackObject] +public class RPCMessage +{ + /// + /// Is this message a response + /// + [Key(0)] + public bool isResponse; + /// + /// Method name + /// + [Key(1)] + public string method; + /// + /// Optional arguments + /// + [Key(2)] + public byte[]? args; +} + +/// +/// IChatRoomService.SendMessage arguments +/// +[MessagePackObject] +public class SendMessageArgs +{ + /// + /// Client ID + /// + [Key(0)] + public int clientId { get; set; } + /// + /// Message contents + /// + [Key(1)] + public string contents { get; set; } + /// + /// Does this message need to be censored? + /// + [Key(2)] + public bool needsToBeCensored { get; set; } +} \ No newline at end of file diff --git a/Lab3/Lab3.sln b/Lab3/Lab3.sln new file mode 100644 index 0000000..9c1103f --- /dev/null +++ b/Lab3/Lab3.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35208.52 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoom", "ChatRoom\ChatRoom.csproj", "{BDE934D4-F8C9-484F-965F-FBBDE10818EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moderator", "Moderator\Moderator.csproj", "{CC693E3A-9508-4D87-8B5F-630DD268858F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Participant", "Participant\Participant.csproj", "{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.Build.0 = Release|Any CPU + {37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.Build.0 = Release|Any CPU + {CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.Build.0 = Release|Any CPU + {F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B2EFC0A1-DB91-4736-B795-29ACE70DCBA4} + EndGlobalSection +EndGlobal diff --git a/Lab3/Moderator/Moderator.cs b/Lab3/Moderator/Moderator.cs new file mode 100644 index 0000000..15587aa --- /dev/null +++ b/Lab3/Moderator/Moderator.cs @@ -0,0 +1,102 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Moderator; + +internal class Moderator +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + /// + /// Run with a given service + /// + /// Chat room service + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + Console.Title = $"Moderator | {name} | {clientId}"; + + while (true) + { + var message = chatRoom.GetNewMessage(); + if (message != null) + { + log.Info($"Checking message ({message.id}): {message.contents}"); + Thread.Sleep(500); + + if (message.needsToBeCensored) + { + chatRoom.RejectMessage(message.id); + } + else + { + chatRoom.ApproveMessage(message.id); + } + } + + Thread.Sleep(1 * 1000); + } + } + + /// + /// Main loop + /// + private void Run() + { + ConfigureLogging(); + + while (true) + { + var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName); + + try + { + RunConnection(chatRoom); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + /// + /// Entry point + /// + static void Main() + { + var self = new Moderator(); + self.Run(); + } +} diff --git a/Lab3/Moderator/Moderator.csproj b/Lab3/Moderator/Moderator.csproj new file mode 100644 index 0000000..ac175f5 --- /dev/null +++ b/Lab3/Moderator/Moderator.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab3/Participant/Participant.cs b/Lab3/Participant/Participant.cs new file mode 100644 index 0000000..44d579c --- /dev/null +++ b/Lab3/Participant/Participant.cs @@ -0,0 +1,107 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Participant; + +internal class Participant +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + /// + /// Run with a given service + /// + /// Chat room service + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + var rnd = new Random(); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + while (true) + { + int strikes = chatRoom.GetStrikes(clientId); + Console.Title = $"Participant | {name} | {clientId} | {strikes} Strikes"; + + var message = string.Join(" ", faker.Lorem.Words(5)); + bool needsToBeCensored = rnd.Next(0, 100) > 50; + if (chatRoom.SendMessage(clientId, message, needsToBeCensored)) + { + log.Info("Sent message"); + } + else + { + log.Info("Failed to send message, blocked"); + var blockedUntil = chatRoom.GetBlockedUntil(clientId); + var now = DateTime.UtcNow; + if (blockedUntil != null && blockedUntil > now) + { + var delta = blockedUntil.Value - now; + log.Info($"Waiting {delta.TotalSeconds:F3}s until block expires"); + Thread.Sleep((int)delta.TotalMilliseconds); + } + } + + Thread.Sleep(2 * 1000); + } + } + + /// + /// Main loop + /// + private void Run() + { + ConfigureLogging(); + + while (true) + { + var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName); + + try + { + RunConnection(chatRoom); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + /// + /// Entry point + /// + static void Main() + { + var self = new Participant(); + self.Run(); + } +} diff --git a/Lab3/Participant/Participant.csproj b/Lab3/Participant/Participant.csproj new file mode 100644 index 0000000..ac175f5 --- /dev/null +++ b/Lab3/Participant/Participant.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + +