From ac702ce8f302c5f69235b05f74f1e8168e46f741 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 7 Dec 2024 17:21:09 +0200 Subject: [PATCH] complete lab4 --- Lab1/ChatRoom/Properties/launchSettings.json | 12 + .../ChatRoom/Properties/launchSettings.json | 12 + .../ChatRoomContract/ChatRoomContract.csproj | 6 +- Lab4/.vscode/launch.json | 104 ++ Lab4/.vscode/tasks.json | 113 ++ Lab4/ChatRoom/ChatRoom.csproj | 22 + Lab4/ChatRoom/ChatRoomGrpcService.cs | 113 ++ Lab4/ChatRoom/ChatRoomLogic.cs | 296 ++++++ Lab4/ChatRoom/ChatRoomRabbitMQService.cs | 159 +++ Lab4/ChatRoom/ChatRoomRestController.cs | 103 ++ Lab4/ChatRoom/ChatRoomSimpleRPCService.cs | 46 + Lab4/ChatRoom/ChatRoomState.cs | 90 ++ Lab4/ChatRoom/Properties/launchSettings.json | 12 + Lab4/ChatRoom/Server.cs | 197 ++++ Lab4/ChatRoom/appsettings.json | 11 + Lab4/ChatRoomContract/ChatRoomContract.csproj | 42 + Lab4/ChatRoomContract/ChatRoomGrpcClient.cs | 116 +++ .../ChatRoomRabbitMQClient.cs | 243 +++++ Lab4/ChatRoomContract/ChatRoomRestClient.cs | 124 +++ Lab4/ChatRoomContract/IChatRoomContract.cs | 79 ++ Lab4/ChatRoomContract/NswagChatRoomClient.cs | 966 ++++++++++++++++++ Lab4/ChatRoomContract/Protos/service.proto | 61 ++ Lab4/ChatRoomContract/RPCMessage.cs | 49 + Lab4/ChatRoomContract/RabbitMQConfig.cs | 30 + Lab4/ChatRoomContract/nswag.json | 19 + Lab4/Lab4.sln | 91 ++ Lab4/Moderator/Moderator.csproj | 18 + Lab4/Moderator/ModeratorLogic.cs | 49 + Lab4/ModeratorGrpc/Moderator.cs | 52 + Lab4/ModeratorGrpc/ModeratorGrpc.csproj | 19 + Lab4/ModeratorRabbitMQ/Moderator.cs | 61 ++ .../ModeratorRabbitMQ.csproj | 19 + Lab4/ModeratorRest/Moderator.cs | 52 + Lab4/ModeratorRest/ModeratorRest.csproj | 19 + Lab4/ModeratorSimpleRPC/Moderator.cs | 81 ++ .../ModeratorSimpleRPC.csproj | 22 + Lab4/Participant/Participant.csproj | 18 + Lab4/Participant/ParticipantLogic.cs | 54 + Lab4/ParticipantGrpc/Participant.cs | 53 + Lab4/ParticipantGrpc/ParticipantGrpc.csproj | 19 + Lab4/ParticipantRabbitMQ/Participant.cs | 60 ++ .../ParticipantRabbitMQ.csproj | 19 + Lab4/ParticipantRest/Participant.cs | 52 + Lab4/ParticipantRest/ParticipantRest.csproj | 19 + Lab4/ParticipantSimpleRPC/Participant.cs | 81 ++ .../ParticipantSimpleRPC.csproj | 22 + 46 files changed, 3902 insertions(+), 3 deletions(-) create mode 100644 Lab1/ChatRoom/Properties/launchSettings.json create mode 100644 Lab2-grpc/ChatRoom/Properties/launchSettings.json create mode 100644 Lab4/.vscode/launch.json create mode 100644 Lab4/.vscode/tasks.json create mode 100644 Lab4/ChatRoom/ChatRoom.csproj create mode 100644 Lab4/ChatRoom/ChatRoomGrpcService.cs create mode 100644 Lab4/ChatRoom/ChatRoomLogic.cs create mode 100644 Lab4/ChatRoom/ChatRoomRabbitMQService.cs create mode 100644 Lab4/ChatRoom/ChatRoomRestController.cs create mode 100644 Lab4/ChatRoom/ChatRoomSimpleRPCService.cs create mode 100644 Lab4/ChatRoom/ChatRoomState.cs create mode 100644 Lab4/ChatRoom/Properties/launchSettings.json create mode 100644 Lab4/ChatRoom/Server.cs create mode 100644 Lab4/ChatRoom/appsettings.json create mode 100644 Lab4/ChatRoomContract/ChatRoomContract.csproj create mode 100644 Lab4/ChatRoomContract/ChatRoomGrpcClient.cs create mode 100644 Lab4/ChatRoomContract/ChatRoomRabbitMQClient.cs create mode 100644 Lab4/ChatRoomContract/ChatRoomRestClient.cs create mode 100644 Lab4/ChatRoomContract/IChatRoomContract.cs create mode 100644 Lab4/ChatRoomContract/NswagChatRoomClient.cs create mode 100644 Lab4/ChatRoomContract/Protos/service.proto create mode 100644 Lab4/ChatRoomContract/RPCMessage.cs create mode 100644 Lab4/ChatRoomContract/RabbitMQConfig.cs create mode 100644 Lab4/ChatRoomContract/nswag.json create mode 100644 Lab4/Lab4.sln create mode 100644 Lab4/Moderator/Moderator.csproj create mode 100644 Lab4/Moderator/ModeratorLogic.cs create mode 100644 Lab4/ModeratorGrpc/Moderator.cs create mode 100644 Lab4/ModeratorGrpc/ModeratorGrpc.csproj create mode 100644 Lab4/ModeratorRabbitMQ/Moderator.cs create mode 100644 Lab4/ModeratorRabbitMQ/ModeratorRabbitMQ.csproj create mode 100644 Lab4/ModeratorRest/Moderator.cs create mode 100644 Lab4/ModeratorRest/ModeratorRest.csproj create mode 100644 Lab4/ModeratorSimpleRPC/Moderator.cs create mode 100644 Lab4/ModeratorSimpleRPC/ModeratorSimpleRPC.csproj create mode 100644 Lab4/Participant/Participant.csproj create mode 100644 Lab4/Participant/ParticipantLogic.cs create mode 100644 Lab4/ParticipantGrpc/Participant.cs create mode 100644 Lab4/ParticipantGrpc/ParticipantGrpc.csproj create mode 100644 Lab4/ParticipantRabbitMQ/Participant.cs create mode 100644 Lab4/ParticipantRabbitMQ/ParticipantRabbitMQ.csproj create mode 100644 Lab4/ParticipantRest/Participant.cs create mode 100644 Lab4/ParticipantRest/ParticipantRest.csproj create mode 100644 Lab4/ParticipantSimpleRPC/Participant.cs create mode 100644 Lab4/ParticipantSimpleRPC/ParticipantSimpleRPC.csproj diff --git a/Lab1/ChatRoom/Properties/launchSettings.json b/Lab1/ChatRoom/Properties/launchSettings.json new file mode 100644 index 0000000..e4abc28 --- /dev/null +++ b/Lab1/ChatRoom/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ChatRoom": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:65375;http://localhost:65376" + } + } +} \ No newline at end of file diff --git a/Lab2-grpc/ChatRoom/Properties/launchSettings.json b/Lab2-grpc/ChatRoom/Properties/launchSettings.json new file mode 100644 index 0000000..db7c427 --- /dev/null +++ b/Lab2-grpc/ChatRoom/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ChatRoom": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56849;http://localhost:56850" + } + } +} \ No newline at end of file diff --git a/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj b/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj index 7d299f7..f260b8c 100644 --- a/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj +++ b/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj @@ -16,13 +16,13 @@ - - diff --git a/Lab4/.vscode/launch.json b/Lab4/.vscode/launch.json new file mode 100644 index 0000000..c84acf9 --- /dev/null +++ b/Lab4/.vscode/launch.json @@ -0,0 +1,104 @@ +{ + "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": "ModeratorGrpc", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ModeratorGrpc", + "program": "${workspaceFolder}/ModeratorGrpc/bin/Debug/net6.0/ModeratorGrpc.dll", + "args": [], + "cwd": "${workspaceFolder}/ModeratorGrpc", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ParticipantGrpc", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ParticipantGrpc", + "program": "${workspaceFolder}/ParticipantGrpc/bin/Debug/net6.0/ParticipantGrpc.dll", + "args": [], + "cwd": "${workspaceFolder}/ParticipantGrpc", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ModeratorRest", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ModeratorRest", + "program": "${workspaceFolder}/ModeratorRest/bin/Debug/net6.0/ModeratorRest.dll", + "args": [], + "cwd": "${workspaceFolder}/ModeratorRest", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ParticipantRest", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ParticipantRest", + "program": "${workspaceFolder}/ParticipantRest/bin/Debug/net6.0/ParticipantRest.dll", + "args": [], + "cwd": "${workspaceFolder}/ParticipantRest", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ModeratorSimpleRPC", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ModeratorSimpleRPC", + "program": "${workspaceFolder}/ModeratorSimpleRPC/bin/Debug/net6.0/ModeratorSimpleRPC.dll", + "args": [], + "cwd": "${workspaceFolder}/ModeratorSimpleRPC", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ParticipantSimpleRPC", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ParticipantSimpleRPC", + "program": "${workspaceFolder}/ParticipantSimpleRPC/bin/Debug/net6.0/ParticipantSimpleRPC.dll", + "args": [], + "cwd": "${workspaceFolder}/ParticipantSimpleRPC", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ModeratorRabbitMQ", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ModeratorRabbitMQ", + "program": "${workspaceFolder}/ModeratorRabbitMQ/bin/Debug/net6.0/ModeratorRabbitMQ.dll", + "args": [], + "cwd": "${workspaceFolder}/ModeratorRabbitMQ", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "ParticipantRabbitMQ", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ParticipantRabbitMQ", + "program": "${workspaceFolder}/ParticipantRabbitMQ/bin/Debug/net6.0/ParticipantRabbitMQ.dll", + "args": [], + "cwd": "${workspaceFolder}/ParticipantRabbitMQ", + "console": "externalTerminal", + "stopAtEntry": false + } + ] +} \ No newline at end of file diff --git a/Lab4/.vscode/tasks.json b/Lab4/.vscode/tasks.json new file mode 100644 index 0000000..41b1f2b --- /dev/null +++ b/Lab4/.vscode/tasks.json @@ -0,0 +1,113 @@ +{ + "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-ModeratorGrpc", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ModeratorRest/ModeratorRest.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ParticipantRest", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ParticipantRest/ParticipantRest.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ModeratorGrpc", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ModeratorGrpc/ModeratorGrpc.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ParticipantGrpc", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ParticipantGrpc/ParticipantGrpc.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ModeratorSimpleRPC", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ModeratorSimpleRPC/ModeratorSimpleRPC.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ParticipantSimpleRPC", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ParticipantSimpleRPC/ParticipantSimpleRPC.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ModeratorRabbitMQ", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ModeratorRabbitMQ/ModeratorRabbitMQ.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ParticipantRabbitMQ", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ParticipantRabbitMQ/ParticipantRabbitMQ.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Lab4/ChatRoom/ChatRoom.csproj b/Lab4/ChatRoom/ChatRoom.csproj new file mode 100644 index 0000000..cf8505e --- /dev/null +++ b/Lab4/ChatRoom/ChatRoom.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + True + + + + + + + + + + + + + + + diff --git a/Lab4/ChatRoom/ChatRoomGrpcService.cs b/Lab4/ChatRoom/ChatRoomGrpcService.cs new file mode 100644 index 0000000..8055b3a --- /dev/null +++ b/Lab4/ChatRoom/ChatRoomGrpcService.cs @@ -0,0 +1,113 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using ChatRoomContract.Protocol; + +namespace ChatRoom; + +public class ChatRoomGrpcService : ChatRoomContract.Protocol.ChatRoom.ChatRoomBase +{ + private readonly ChatRoomLogic logic; + public ChatRoomGrpcService(ChatRoomLogic logic) + { + this.logic = logic; + } + + /// + /// Register client with a name + /// + /// Name of client. + /// Call context. + /// Client ID + public override Task RegisterClient(RegisterClientRequest request, ServerCallContext context) + { + var result = new ClientId { Id = logic.RegisterClient(request.Name) }; + return Task.FromResult(result); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Call context. + /// Number of strikes + public override Task GetStrikes(ClientId request, ServerCallContext context) + { + var result = new Srikes { Strikes = logic.GetStrikes(request.Id) }; + return Task.FromResult(result); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Call context. + /// Optional datetime object + public override Task GetBlockedUntil(ClientId request, ServerCallContext context) + { + var timestamp = logic.GetBlockedUntil(request.Id); + var result = new BlockedUntil { HasTimestamp = false }; + if (timestamp != null) + { + result.HasTimestamp = true; + result.Timestamp = Timestamp.FromDateTime(timestamp.Value); + } + return Task.FromResult(result); + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Message details + /// Call context. + /// Was sending successful, can fail if user is blocked + public override Task SendMessage(UserMessageRequest request, ServerCallContext context) + { + var success = logic.SendMessage(request.ClientId, request.Contents, request.NeedsToBeCensored); + var result = new BoolResponse { Success = success }; + return Task.FromResult(result); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Empty + /// Call context. + /// Message object. Returns null if there is no message + public override Task GetNewMessage(Empty request, ServerCallContext context) + { + var message = logic.GetNewMessage(); + var result = new NewUserMessage { HasMessage = false, Message = new UserMessage() }; + if (message != null) + { + result.HasMessage = true; + result.Message.Id = message.id; + result.Message.Contents = message.contents; + result.Message.NeedsToBeCensored = message.needsToBeCensored; + } + return Task.FromResult(result); + } + + /// + /// Reject a message + /// + /// Message ID + /// Call context. + /// Empty + public override Task ApproveMessage(MessageId request, ServerCallContext context) + { + logic.ApproveMessage(request.Id); + return Task.FromResult(new Empty()); + } + + /// + /// Approve a message + /// + /// Message ID + /// Call context. + /// Empty + public override Task RejectMessage(MessageId request, ServerCallContext context) + { + logic.RejectMessage(request.Id); + return Task.FromResult(new Empty()); + } +} diff --git a/Lab4/ChatRoom/ChatRoomLogic.cs b/Lab4/ChatRoom/ChatRoomLogic.cs new file mode 100644 index 0000000..7d58b0f --- /dev/null +++ b/Lab4/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/Lab4/ChatRoom/ChatRoomRabbitMQService.cs b/Lab4/ChatRoom/ChatRoomRabbitMQService.cs new file mode 100644 index 0000000..760e1e2 --- /dev/null +++ b/Lab4/ChatRoom/ChatRoomRabbitMQService.cs @@ -0,0 +1,159 @@ +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 ChatRoomRabbitMQService +{ + /// + /// Logger for this class. + /// + private Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Communications channel to RabbitMQ message broker. + /// + private IModel rmqChannel; + + /// + /// Service logic. + /// + private ChatRoomLogic logic; + + public ChatRoomRabbitMQService(ChatRoomLogic logic, IConnection connection, string exchangeName, string serverQueueName) + { + this.logic = logic; + + //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/Lab4/ChatRoom/ChatRoomRestController.cs b/Lab4/ChatRoom/ChatRoomRestController.cs new file mode 100644 index 0000000..86fd6af --- /dev/null +++ b/Lab4/ChatRoom/ChatRoomRestController.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ChatRoom; + +/// +/// Service. Class must be marked public, otherwise ASP.NET core runtime will not find it. +/// +/// Look into FromXXX attributes if you need to map inputs to custom parts of HTTP request. +/// +[Route("chatRoom")] +[ApiController] +public class ChatRoomRestController : ControllerBase +{ + /// + /// Service logic. This is created in Server.StartServer() and received through DI in constructor. + /// + private readonly ChatRoomLogic logic; + + /// + /// Constructor + /// + /// Logic to use. This will get passed through DI. + public ChatRoomRestController(ChatRoomLogic logic) + { + this.logic = logic; + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + [HttpPost("/registerClient")] + public ActionResult RegisterClient(string name) + { + return logic.RegisterClient(name); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + [HttpGet("/getStrikes")] + public ActionResult GetStrikes(int clientId) + { + return logic.GetStrikes(clientId); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + [HttpGet("/getBlockedUntil")] + public ActionResult GetBlockedUntil(int clientId) + { + return logic.GetBlockedUntil(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 + [HttpPost("/sendMessage")] + public ActionResult SendMessage(int clientId, string contents, bool needsToBeCensored) + { + return logic.SendMessage(clientId, contents, needsToBeCensored); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + [HttpGet("/getNewMessage")] + public ActionResult GetNewMessage() + { + return logic.GetNewMessage(); + } + + /// + /// Reject a message + /// + /// Message ID + [HttpPost("/rejectMessage")] + public void RejectMessage(int messageId) + { + logic.RejectMessage(messageId); + } + + /// + /// Approve a message + /// + /// Message ID + [HttpPost("/approveMessage")] + public void ApproveMessage(int messageId) + { + logic.ApproveMessage(messageId); + } +} diff --git a/Lab4/ChatRoom/ChatRoomSimpleRPCService.cs b/Lab4/ChatRoom/ChatRoomSimpleRPCService.cs new file mode 100644 index 0000000..98b0ea1 --- /dev/null +++ b/Lab4/ChatRoom/ChatRoomSimpleRPCService.cs @@ -0,0 +1,46 @@ +namespace ChatRoom; + +public class ChatRoomSimpleRPCService : ChatRoomContract.IChatRoomService +{ + private readonly ChatRoomLogic logic; + + public ChatRoomSimpleRPCService(ChatRoomLogic logic) + { + this.logic = logic; + } + + public int RegisterClient(string name) + { + return logic.RegisterClient(name); + } + + public void ApproveMessage(int messageId) + { + logic.ApproveMessage(messageId); + } + + public ChatRoomContract.Message? GetNewMessage() + { + return logic.GetNewMessage(); + } + + public int GetStrikes(int clientId) + { + return logic.GetStrikes(clientId); + } + + public void RejectMessage(int messageId) + { + logic.RejectMessage(messageId); + } + + public bool SendMessage(int clientId, string contents, bool needsToBeCensored) + { + return logic.SendMessage(clientId, contents, needsToBeCensored); + } + + public DateTime? GetBlockedUntil(int clientId) + { + return logic.GetBlockedUntil(clientId); + } +} diff --git a/Lab4/ChatRoom/ChatRoomState.cs b/Lab4/ChatRoom/ChatRoomState.cs new file mode 100644 index 0000000..4e6f3c2 --- /dev/null +++ b/Lab4/ChatRoom/ChatRoomState.cs @@ -0,0 +1,90 @@ +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/Lab4/ChatRoom/Properties/launchSettings.json b/Lab4/ChatRoom/Properties/launchSettings.json new file mode 100644 index 0000000..5d83feb --- /dev/null +++ b/Lab4/ChatRoom/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ChatRoom": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:65375;http://localhost:65376" + } + } +} \ No newline at end of file diff --git a/Lab4/ChatRoom/Server.cs b/Lab4/ChatRoom/Server.cs new file mode 100644 index 0000000..013f8fc --- /dev/null +++ b/Lab4/ChatRoom/Server.cs @@ -0,0 +1,197 @@ +using System.Net; + +using NLog; + +using SimpleRpc.Transports; +using SimpleRpc.Transports.Http.Server; +using SimpleRpc.Serialization.Hyperion; +using ChatRoomContract; +using Microsoft.Extensions.Logging; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace ChatRoom; + +public class Server +{ + /// + /// Logger for this class. + /// + 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 entry point. + /// + /// Command line arguments. + public static void Main(string[] args) + { + var self = new Server(); + self.Run(args); + } + + /// + /// Program body. + /// + /// Command line arguments. + private void Run(string[] args) + { + //configure logging + ConfigureLogging(); + + //indicate server is about to start + log.Info("Server is about to start"); + + //start the server + StartServer(args); + } + + /// + /// Starts integrated server. + /// + /// Command line arguments. + private void StartServer(string[] args) + { + Console.Title = "Chat Room"; + + var logic = new ChatRoomLogic(); + + // Simple RPC + WebApplication simpleRPCWebApplication; + { + ///create web app builder + var builder = WebApplication.CreateBuilder(args); + + //configure integrated server + builder.WebHost.ConfigureKestrel(opts => { + opts.Listen(IPAddress.Loopback, 5001); + }); + + //add SimpleRPC services + builder.Services + .AddSimpleRpcServer(new HttpServerTransportOptions { Path = "/simplerpc" }) + .AddSimpleRpcHyperionSerializer(); + + //add our custom services + builder.Services.AddSingleton(new ChatRoomSimpleRPCService(logic)); + + //build the server + var app = builder.Build(); + + //add SimpleRPC middleware + app.UseSimpleRpcServer(); + + simpleRPCWebApplication = app; + } + + // Rest, GRPC + WebApplication restGrpcWebApplication; + { + ///create web app builder + var builder = WebApplication.CreateBuilder(args); + + //configure integrated server + builder.WebHost.ConfigureKestrel(opts => { + opts.Listen(IPAddress.Loopback, 5000, opts => + { + opts.Protocols = HttpProtocols.Http2; + }); + }); + + //add and configure swagger documentation generator (http://127.0.0.1:5000/swagger/) + builder.Services.AddSwaggerGen(opts => + { + //include code comments in swagger documentation + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + opts.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); + + //turn on support for web api controllers + builder.Services + .AddControllers() + .AddJsonOptions(opts => + { + //this makes enumeration values to be strings instead of integers in opeanapi doc + opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + //add CORS policies + builder.Services.AddCors(cr => + { + //allow everything from everywhere + cr.AddPolicy("allowAll", cp => + { + cp.AllowAnyOrigin(); + cp.AllowAnyMethod(); + cp.AllowAnyHeader(); + }); + }); + + //publish the background logic as (an internal) service through dependency injection, + //otherwise it will not start until the first client calls into controller + builder.Services.AddSingleton(logic); + + //add support for GRPC services + builder.Services.AddGrpc(); + + builder.Services.AddSingleton(new ChatRoomGrpcService(logic)); + + //build the server + var app = builder.Build(); + + //turn CORS policy on + app.UseCors("allowAll"); + + //turn on support for swagger doc web page + app.UseSwagger(); + app.UseSwaggerUI(); + + app.MapGrpcService(); + + ////turn on request routing + app.UseRouting(); + + ////configure routes + app.MapControllerRoute( + name: "default", + pattern: "{controller}/{action=Index}/{id?}" + ); + + restGrpcWebApplication = app; + } + + // RabbitMQ + { + var service = new ChatRoomRabbitMQService( + logic, + RabbitMQConfig.CreateConnection(), + RabbitMQConfig.ExchangeName, + RabbitMQConfig.ServerQueueName + ); + } + + var restGrpcTask = restGrpcWebApplication.RunAsync(); + var simpleRPCTask = simpleRPCWebApplication.RunAsync(); + + restGrpcTask.Wait(); + simpleRPCTask.Wait(); + } +} diff --git a/Lab4/ChatRoom/appsettings.json b/Lab4/ChatRoom/appsettings.json new file mode 100644 index 0000000..ae459eb --- /dev/null +++ b/Lab4/ChatRoom/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Lab4/ChatRoomContract/ChatRoomContract.csproj b/Lab4/ChatRoomContract/ChatRoomContract.csproj new file mode 100644 index 0000000..7fe6a53 --- /dev/null +++ b/Lab4/ChatRoomContract/ChatRoomContract.csproj @@ -0,0 +1,42 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/Lab4/ChatRoomContract/ChatRoomGrpcClient.cs b/Lab4/ChatRoomContract/ChatRoomGrpcClient.cs new file mode 100644 index 0000000..a583e96 --- /dev/null +++ b/Lab4/ChatRoomContract/ChatRoomGrpcClient.cs @@ -0,0 +1,116 @@ + +using ChatRoomContract.Protocol; +using Google.Protobuf.WellKnownTypes; +using Grpc.Net.Client; + +namespace ChatRoomContract; + +public class ChatRoomGrpcClient : IChatRoomService +{ + GrpcChannel channel; + ChatRoom.ChatRoomClient client; + + public ChatRoomGrpcClient(string address) + { + channel = GrpcChannel.ForAddress(address); + client = new ChatRoom.ChatRoomClient(channel); + } + + /// + /// Approve a message + /// + /// Message ID + public void ApproveMessage(int messageId) + { + client.ApproveMessage(new MessageId { Id = messageId }); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + public DateTime? GetBlockedUntil(int clientId) + { + var result = client.GetBlockedUntil(new ClientId { Id = clientId }); + if (result.HasTimestamp) + { + return result.Timestamp.ToDateTime(); + } + else + { + return null; + } + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + public Message? GetNewMessage() + { + var result = client.GetNewMessage(new Empty()); + if (result.HasMessage) + { + return new Message + { + id = result.Message.Id, + contents = result.Message.Contents, + needsToBeCensored = result.Message.NeedsToBeCensored + }; + } + else + { + return null; + } + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + public int GetStrikes(int clientId) + { + var result = client.GetStrikes(new ClientId { Id = clientId }); + return result.Strikes; + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + public int RegisterClient(string name) + { + var result = client.RegisterClient(new RegisterClientRequest { Name = name }); + return result.Id; + } + + /// + /// Reject a message + /// + /// Message ID + public void RejectMessage(int messageId) + { + client.RejectMessage(new MessageId { Id = 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 result = client.SendMessage(new UserMessageRequest + { + ClientId = clientId, + Contents = contents, + NeedsToBeCensored = needsToBeCensored + }); + return result.Success; + } +} \ No newline at end of file diff --git a/Lab4/ChatRoomContract/ChatRoomRabbitMQClient.cs b/Lab4/ChatRoomContract/ChatRoomRabbitMQClient.cs new file mode 100644 index 0000000..66427c8 --- /dev/null +++ b/Lab4/ChatRoomContract/ChatRoomRabbitMQClient.cs @@ -0,0 +1,243 @@ +using MessagePack; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace ChatRoomContract; + +/// +/// RabbitMQ chat room client +/// +public class ChatRoomRabbitMQClient : 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 ChatRoomRabbitMQClient(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/Lab4/ChatRoomContract/ChatRoomRestClient.cs b/Lab4/ChatRoomContract/ChatRoomRestClient.cs new file mode 100644 index 0000000..dbc2c59 --- /dev/null +++ b/Lab4/ChatRoomContract/ChatRoomRestClient.cs @@ -0,0 +1,124 @@ + +using NswagGenerated; + +namespace ChatRoomContract; + +/// +/// Wrapper around generated NSwag client +/// +public class ChatRoomRestClient : IChatRoomService +{ + NswagGenerated.NswagChatRoomClient nswag; + + /// + /// Chat room client constructor + /// + /// Server base url + /// HTTP Client + public ChatRoomRestClient(string baseUrl, HttpClient httpClient) + { + nswag = new NswagGenerated.NswagChatRoomClient(baseUrl, httpClient); + } + + /// + /// Approve a message + /// + /// Message ID + public void ApproveMessage(int messageId) + { + nswag.ApproveMessage(messageId); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + public DateTime? GetBlockedUntil(int clientId) + { + try + { + var offset = nswag.GetBlockedUntil(clientId); + return offset.DateTime; + } + catch (ApiException e) + { + if (e.StatusCode == 204) + { + return null; + } + + throw; + } + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + public Message? GetNewMessage() + { + NswagGenerated.Message message; + try + { + message = nswag.GetNewMessage(); + } + catch (ApiException e) + { + if (e.StatusCode == 204) + { + return null; + } + + throw; + } + + return new Message + { + id = message.Id, + contents = message.Contents, + needsToBeCensored = message.NeedsToBeCensored + }; + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + public int GetStrikes(int clientId) + { + return nswag.GetStrikes(clientId); + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + public int RegisterClient(string name) + { + return nswag.RegisterClient(name); + } + + /// + /// Reject a message + /// + /// Message ID + public void RejectMessage(int messageId) + { + nswag.RejectMessage(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) + { + return nswag.SendMessage(clientId, contents, needsToBeCensored); + } +} \ No newline at end of file diff --git a/Lab4/ChatRoomContract/IChatRoomContract.cs b/Lab4/ChatRoomContract/IChatRoomContract.cs new file mode 100644 index 0000000..a5d9b11 --- /dev/null +++ b/Lab4/ChatRoomContract/IChatRoomContract.cs @@ -0,0 +1,79 @@ +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/Lab4/ChatRoomContract/NswagChatRoomClient.cs b/Lab4/ChatRoomContract/NswagChatRoomClient.cs new file mode 100644 index 0000000..9256f31 --- /dev/null +++ b/Lab4/ChatRoomContract/NswagChatRoomClient.cs @@ -0,0 +1,966 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace NswagGenerated +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class NswagChatRoomClient + { +#pragma warning disable 8618 + private string _baseUrl; +#pragma warning restore 8618 + + private System.Net.Http.HttpClient _httpClient; + private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + private Newtonsoft.Json.JsonSerializerSettings _instanceSettings; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public NswagChatRoomClient(string baseUrl, System.Net.Http.HttpClient httpClient) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + BaseUrl = baseUrl; + _httpClient = httpClient; + Initialize(); + } + + private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() + { + var settings = new Newtonsoft.Json.JsonSerializerSettings(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + public string BaseUrl + { + get { return _baseUrl; } + set + { + _baseUrl = value; + if (!string.IsNullOrEmpty(_baseUrl) && !_baseUrl.EndsWith("/")) + _baseUrl += '/'; + } + } + + protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _instanceSettings ?? _settings.Value; } } + + static partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings); + + partial void Initialize(); + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task RegisterClientAsync(string name) + { + return RegisterClientAsync(name, System.Threading.CancellationToken.None); + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual int RegisterClient(string name) + { + return System.Threading.Tasks.Task.Run(async () => await RegisterClientAsync(name, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task RegisterClientAsync(string name, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "text/plain"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "registerClient" + urlBuilder_.Append("registerClient"); + urlBuilder_.Append('?'); + if (name != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("name")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetStrikesAsync(int? clientId) + { + return GetStrikesAsync(clientId, System.Threading.CancellationToken.None); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual int GetStrikes(int? clientId) + { + return System.Threading.Tasks.Task.Run(async () => await GetStrikesAsync(clientId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetStrikesAsync(int? clientId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getStrikes" + urlBuilder_.Append("getStrikes"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetBlockedUntilAsync(int? clientId) + { + return GetBlockedUntilAsync(clientId, System.Threading.CancellationToken.None); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.DateTimeOffset GetBlockedUntil(int? clientId) + { + return System.Threading.Tasks.Task.Run(async () => await GetBlockedUntilAsync(clientId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetBlockedUntilAsync(int? clientId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getBlockedUntil" + urlBuilder_.Append("getBlockedUntil"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task SendMessageAsync(int? clientId, string contents, bool? needsToBeCensored) + { + return SendMessageAsync(clientId, contents, needsToBeCensored, System.Threading.CancellationToken.None); + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual bool SendMessage(int? clientId, string contents, bool? needsToBeCensored) + { + return System.Threading.Tasks.Task.Run(async () => await SendMessageAsync(clientId, contents, needsToBeCensored, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task SendMessageAsync(int? clientId, string contents, bool? needsToBeCensored, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "text/plain"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "sendMessage" + urlBuilder_.Append("sendMessage"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (contents != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("contents")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(contents, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (needsToBeCensored != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("needsToBeCensored")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(needsToBeCensored, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetNewMessageAsync() + { + return GetNewMessageAsync(System.Threading.CancellationToken.None); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual Message GetNewMessage() + { + return System.Threading.Tasks.Task.Run(async () => await GetNewMessageAsync(System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetNewMessageAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getNewMessage" + urlBuilder_.Append("getNewMessage"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task RejectMessageAsync(int? messageId) + { + return RejectMessageAsync(messageId, System.Threading.CancellationToken.None); + } + + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual void RejectMessage(int? messageId) + { + System.Threading.Tasks.Task.Run(async () => await RejectMessageAsync(messageId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task RejectMessageAsync(int? messageId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "rejectMessage" + urlBuilder_.Append("rejectMessage"); + urlBuilder_.Append('?'); + if (messageId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("messageId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(messageId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + return; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task ApproveMessageAsync(int? messageId) + { + return ApproveMessageAsync(messageId, System.Threading.CancellationToken.None); + } + + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual void ApproveMessage(int? messageId) + { + System.Threading.Tasks.Task.Run(async () => await ApproveMessageAsync(messageId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task ApproveMessageAsync(int? messageId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "approveMessage" + urlBuilder_.Append("approveMessage"); + urlBuilder_.Append('?'); + if (messageId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("messageId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(messageId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + return; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T), string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody, responseText); + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var streamReader = new System.IO.StreamReader(responseStream)) + using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader)) + { + var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings); + var typedBody = serializer.Deserialize(jsonTextReader); + return new ObjectResponseResult(typedBody, string.Empty); + } + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[])value); + } + else if (value is string[]) + { + return string.Join(",", (string[])value); + } + else if (value.GetType().IsArray) + { + var valueArray = (System.Array)value; + var valueTextArray = new string[valueArray.Length]; + for (var i = 0; i < valueArray.Length; i++) + { + valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo); + } + return string.Join(",", valueTextArray); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Message + { + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public int Id { get; set; } + + [Newtonsoft.Json.JsonProperty("contents", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Contents { get; set; } + + [Newtonsoft.Json.JsonProperty("needsToBeCensored", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool NeedsToBeCensored { get; set; } + + } + + + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : System.Exception + { + public int StatusCode { get; private set; } + + public string Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : ApiException + { + public TResult Result { get; private set; } + + public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/Lab4/ChatRoomContract/Protos/service.proto b/Lab4/ChatRoomContract/Protos/service.proto new file mode 100644 index 0000000..66c05f8 --- /dev/null +++ b/Lab4/ChatRoomContract/Protos/service.proto @@ -0,0 +1,61 @@ +//set the language version +syntax = "proto3"; + +//this will translate into C# namespace +package ChatRoomContract.Protocol; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +service ChatRoom { + rpc RegisterClient(RegisterClientRequest) returns (ClientId); + rpc GetStrikes(ClientId) returns (Srikes); + rpc GetBlockedUntil(ClientId) returns (BlockedUntil); + rpc SendMessage(UserMessageRequest) returns (BoolResponse); + rpc GetNewMessage(google.protobuf.Empty) returns (NewUserMessage); + rpc RejectMessage(MessageId) returns (google.protobuf.Empty); + rpc ApproveMessage(MessageId) returns (google.protobuf.Empty); +} + +message BoolResponse { + bool success = 1; +} + +message RegisterClientRequest { + string name = 1; +} + +message Srikes { + int32 strikes = 1; +} + +message ClientId { + int32 id = 1; +} + +message MessageId { + int32 id = 1; +} + +message BlockedUntil { + bool hasTimestamp = 1; + google.protobuf.Timestamp timestamp = 2; +} + +message UserMessageRequest { + int32 clientId = 1; + string contents = 2; + bool needsToBeCensored = 3; +} + + +message UserMessage { + int32 id = 1; + string contents = 2; + bool needsToBeCensored = 3; +} + +message NewUserMessage { + bool hasMessage = 1; + UserMessage message = 2; +} \ No newline at end of file diff --git a/Lab4/ChatRoomContract/RPCMessage.cs b/Lab4/ChatRoomContract/RPCMessage.cs new file mode 100644 index 0000000..2acc5b1 --- /dev/null +++ b/Lab4/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/Lab4/ChatRoomContract/RabbitMQConfig.cs b/Lab4/ChatRoomContract/RabbitMQConfig.cs new file mode 100644 index 0000000..8a1fe2a --- /dev/null +++ b/Lab4/ChatRoomContract/RabbitMQConfig.cs @@ -0,0 +1,30 @@ +using RabbitMQ.Client; + +namespace ChatRoomContract; + +public static class RabbitMQConfig +{ + 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/Lab4/ChatRoomContract/nswag.json b/Lab4/ChatRoomContract/nswag.json new file mode 100644 index 0000000..921b2a1 --- /dev/null +++ b/Lab4/ChatRoomContract/nswag.json @@ -0,0 +1,19 @@ +{ + "runtime": "Net60", + "documentGenerator": { + "fromDocument": { + "url": "http://localhost:5000/swagger/v1/swagger.json", + "output": null + } + }, + + "codeGenerators": { + "openApiToCSharpClient": { + "classname" : "NswagChatRoomClient", + "namespace" : "NswagGenerated", + "output" : "NswagChatRoomClient.cs", + + "generateSyncMethods": true + } + } +} \ No newline at end of file diff --git a/Lab4/Lab4.sln b/Lab4/Lab4.sln new file mode 100644 index 0000000..36242a4 --- /dev/null +++ b/Lab4/Lab4.sln @@ -0,0 +1,91 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35208.52 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatRoom", "ChatRoom\ChatRoom.csproj", "{0F2C1DA4-5EC0-4287-A27D-5DA3C2FF7F62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{301234B6-13D6-4947-BCEC-1287E7CFCA6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModeratorSimpleRPC", "ModeratorSimpleRPC\ModeratorSimpleRPC.csproj", "{1258DF7D-83C6-49AF-8A47-4A06F816846F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticipantSimpleRPC", "ParticipantSimpleRPC\ParticipantSimpleRPC.csproj", "{1228253D-45D1-477F-93F3-99C2741CF827}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModeratorRest", "ModeratorRest\ModeratorRest.csproj", "{64B1DFC1-B9AC-46D5-951E-7B66EFC47DA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticipantRest", "ParticipantRest\ParticipantRest.csproj", "{8C97E0F7-5438-4B30-B3F7-DB1757E784C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moderator", "Moderator\Moderator.csproj", "{6DFD9D6C-B52B-44C4-8246-6ACC970A55F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Participant", "Participant\Participant.csproj", "{B307B77D-47D1-4603-AF04-35F9D5F2BDE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModeratorGrpc", "ModeratorGrpc\ModeratorGrpc.csproj", "{906F9E24-D852-44DD-8CB6-9A7EF70A09D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticipantGrpc", "ParticipantGrpc\ParticipantGrpc.csproj", "{C14A7CA4-96DA-498A-B8A9-291AE54A5BD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModeratorRabbitMQ", "ModeratorRabbitMQ\ModeratorRabbitMQ.csproj", "{065DF942-2AFF-44A0-AA7A-44CCC5446848}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticipantRabbitMQ", "ParticipantRabbitMQ\ParticipantRabbitMQ.csproj", "{9434774F-B80A-490F-A57A-52031CC2F5C6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0F2C1DA4-5EC0-4287-A27D-5DA3C2FF7F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F2C1DA4-5EC0-4287-A27D-5DA3C2FF7F62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F2C1DA4-5EC0-4287-A27D-5DA3C2FF7F62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F2C1DA4-5EC0-4287-A27D-5DA3C2FF7F62}.Release|Any CPU.Build.0 = Release|Any CPU + {301234B6-13D6-4947-BCEC-1287E7CFCA6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {301234B6-13D6-4947-BCEC-1287E7CFCA6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {301234B6-13D6-4947-BCEC-1287E7CFCA6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {301234B6-13D6-4947-BCEC-1287E7CFCA6F}.Release|Any CPU.Build.0 = Release|Any CPU + {1258DF7D-83C6-49AF-8A47-4A06F816846F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1258DF7D-83C6-49AF-8A47-4A06F816846F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1258DF7D-83C6-49AF-8A47-4A06F816846F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1258DF7D-83C6-49AF-8A47-4A06F816846F}.Release|Any CPU.Build.0 = Release|Any CPU + {1228253D-45D1-477F-93F3-99C2741CF827}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1228253D-45D1-477F-93F3-99C2741CF827}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1228253D-45D1-477F-93F3-99C2741CF827}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1228253D-45D1-477F-93F3-99C2741CF827}.Release|Any CPU.Build.0 = Release|Any CPU + {64B1DFC1-B9AC-46D5-951E-7B66EFC47DA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64B1DFC1-B9AC-46D5-951E-7B66EFC47DA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64B1DFC1-B9AC-46D5-951E-7B66EFC47DA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64B1DFC1-B9AC-46D5-951E-7B66EFC47DA6}.Release|Any CPU.Build.0 = Release|Any CPU + {8C97E0F7-5438-4B30-B3F7-DB1757E784C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C97E0F7-5438-4B30-B3F7-DB1757E784C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C97E0F7-5438-4B30-B3F7-DB1757E784C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C97E0F7-5438-4B30-B3F7-DB1757E784C8}.Release|Any CPU.Build.0 = Release|Any CPU + {6DFD9D6C-B52B-44C4-8246-6ACC970A55F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DFD9D6C-B52B-44C4-8246-6ACC970A55F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DFD9D6C-B52B-44C4-8246-6ACC970A55F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DFD9D6C-B52B-44C4-8246-6ACC970A55F8}.Release|Any CPU.Build.0 = Release|Any CPU + {B307B77D-47D1-4603-AF04-35F9D5F2BDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B307B77D-47D1-4603-AF04-35F9D5F2BDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B307B77D-47D1-4603-AF04-35F9D5F2BDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B307B77D-47D1-4603-AF04-35F9D5F2BDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {906F9E24-D852-44DD-8CB6-9A7EF70A09D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {906F9E24-D852-44DD-8CB6-9A7EF70A09D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {906F9E24-D852-44DD-8CB6-9A7EF70A09D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {906F9E24-D852-44DD-8CB6-9A7EF70A09D7}.Release|Any CPU.Build.0 = Release|Any CPU + {C14A7CA4-96DA-498A-B8A9-291AE54A5BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C14A7CA4-96DA-498A-B8A9-291AE54A5BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C14A7CA4-96DA-498A-B8A9-291AE54A5BD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C14A7CA4-96DA-498A-B8A9-291AE54A5BD5}.Release|Any CPU.Build.0 = Release|Any CPU + {065DF942-2AFF-44A0-AA7A-44CCC5446848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {065DF942-2AFF-44A0-AA7A-44CCC5446848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {065DF942-2AFF-44A0-AA7A-44CCC5446848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {065DF942-2AFF-44A0-AA7A-44CCC5446848}.Release|Any CPU.Build.0 = Release|Any CPU + {9434774F-B80A-490F-A57A-52031CC2F5C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9434774F-B80A-490F-A57A-52031CC2F5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9434774F-B80A-490F-A57A-52031CC2F5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9434774F-B80A-490F-A57A-52031CC2F5C6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2D31C66F-9644-49FC-B5F8-3FA3EE49FAC8} + EndGlobalSection +EndGlobal diff --git a/Lab4/Moderator/Moderator.csproj b/Lab4/Moderator/Moderator.csproj new file mode 100644 index 0000000..ba55bb0 --- /dev/null +++ b/Lab4/Moderator/Moderator.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/Moderator/ModeratorLogic.cs b/Lab4/Moderator/ModeratorLogic.cs new file mode 100644 index 0000000..923aa18 --- /dev/null +++ b/Lab4/Moderator/ModeratorLogic.cs @@ -0,0 +1,49 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Moderator; + +public class ModeratorLogic +{ + private IChatRoomService chatRoom; + + public ModeratorLogic(IChatRoomService chatRoom) + { + this.chatRoom = chatRoom; + } + + public void Run() + { + Logger log = LogManager.GetCurrentClassLogger(); + + 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); + } + } +} diff --git a/Lab4/ModeratorGrpc/Moderator.cs b/Lab4/ModeratorGrpc/Moderator.cs new file mode 100644 index 0000000..36bf265 --- /dev/null +++ b/Lab4/ModeratorGrpc/Moderator.cs @@ -0,0 +1,52 @@ +using ChatRoomContract; +using NLog; +using Moderator; + +namespace ModeratorGrpc; + +internal class Moderator +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomGrpcClient("http://127.0.0.1:5000"); + var moderator = new ModeratorLogic(client); + + try + { + moderator.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ModeratorGrpc/ModeratorGrpc.csproj b/Lab4/ModeratorGrpc/ModeratorGrpc.csproj new file mode 100644 index 0000000..80134ec --- /dev/null +++ b/Lab4/ModeratorGrpc/ModeratorGrpc.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ModeratorRabbitMQ/Moderator.cs b/Lab4/ModeratorRabbitMQ/Moderator.cs new file mode 100644 index 0000000..66d768a --- /dev/null +++ b/Lab4/ModeratorRabbitMQ/Moderator.cs @@ -0,0 +1,61 @@ +using ChatRoomContract; +using NLog; +using Bogus; +using Moderator; + +namespace ModeratorRabbitMQ; + +internal class Moderator +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + /// + /// Entry point + /// + static void Main() + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var chatRoom = new ChatRoomRabbitMQClient( + RabbitMQConfig.CreateConnection(), + RabbitMQConfig.ExchangeName, + RabbitMQConfig.CreateClientQueueName(), + RabbitMQConfig.ServerQueueName + ); + var moderator = new ModeratorLogic(chatRoom); + + try + { + moderator.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ModeratorRabbitMQ/ModeratorRabbitMQ.csproj b/Lab4/ModeratorRabbitMQ/ModeratorRabbitMQ.csproj new file mode 100644 index 0000000..80134ec --- /dev/null +++ b/Lab4/ModeratorRabbitMQ/ModeratorRabbitMQ.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ModeratorRest/Moderator.cs b/Lab4/ModeratorRest/Moderator.cs new file mode 100644 index 0000000..c74e336 --- /dev/null +++ b/Lab4/ModeratorRest/Moderator.cs @@ -0,0 +1,52 @@ +using ChatRoomContract; +using NLog; +using Moderator; + +namespace ModeratorRest; + +internal class Moderator +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomRestClient("http://127.0.0.1:5000", new HttpClient()); + var moderator = new ModeratorLogic(client); + + try + { + moderator.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ModeratorRest/ModeratorRest.csproj b/Lab4/ModeratorRest/ModeratorRest.csproj new file mode 100644 index 0000000..80134ec --- /dev/null +++ b/Lab4/ModeratorRest/ModeratorRest.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ModeratorSimpleRPC/Moderator.cs b/Lab4/ModeratorSimpleRPC/Moderator.cs new file mode 100644 index 0000000..f362873 --- /dev/null +++ b/Lab4/ModeratorSimpleRPC/Moderator.cs @@ -0,0 +1,81 @@ +using ChatRoomContract; +using Microsoft.Extensions.DependencyInjection; +using NLog; +using SimpleRpc.Transports.Http.Client; +using SimpleRpc.Serialization.Hyperion; +using SimpleRpc.Transports; +using System.Diagnostics; +using Moderator; + +namespace ModeratorSimpleRPC; + +internal class Moderator +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + //configure logging + ConfigureLogging(); + + //initialize random number generator + var rnd = new Random(); + + while (true) + { + //connect to the server, get service client proxy + var sc = new ServiceCollection(); + sc + .AddSimpleRpcClient( + "chatRoomService", //must be same as on line 86 + new HttpClientTransportOptions + { + Url = "http://127.0.0.1:5001/simplerpc", + Serializer = "HyperionMessageSerializer" + } + ) + .AddSimpleRpcHyperionSerializer(); + + sc.AddSimpleRpcProxy("chatRoomService"); //must be same as on line 77 + + var sp = sc.BuildServiceProvider(); + + var chatRoom = sp.GetService(); + Debug.Assert(chatRoom != null); + + var moderator = new ModeratorLogic(chatRoom); + + try + { + moderator.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} + diff --git a/Lab4/ModeratorSimpleRPC/ModeratorSimpleRPC.csproj b/Lab4/ModeratorSimpleRPC/ModeratorSimpleRPC.csproj new file mode 100644 index 0000000..5ad18e7 --- /dev/null +++ b/Lab4/ModeratorSimpleRPC/ModeratorSimpleRPC.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Lab4/Participant/Participant.csproj b/Lab4/Participant/Participant.csproj new file mode 100644 index 0000000..ba55bb0 --- /dev/null +++ b/Lab4/Participant/Participant.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/Participant/ParticipantLogic.cs b/Lab4/Participant/ParticipantLogic.cs new file mode 100644 index 0000000..303f2a9 --- /dev/null +++ b/Lab4/Participant/ParticipantLogic.cs @@ -0,0 +1,54 @@ +using ChatRoomContract; +using Bogus; +using NLog; + +namespace Participant; + +public class ParticipantLogic +{ + private IChatRoomService chatRoom; + + public ParticipantLogic(IChatRoomService chatRoom) + { + this.chatRoom = chatRoom; + } + + public void Run() + { + Logger log = LogManager.GetCurrentClassLogger(); + + 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); + } + } +} diff --git a/Lab4/ParticipantGrpc/Participant.cs b/Lab4/ParticipantGrpc/Participant.cs new file mode 100644 index 0000000..77f5e5f --- /dev/null +++ b/Lab4/ParticipantGrpc/Participant.cs @@ -0,0 +1,53 @@ +using ChatRoomContract; +using NLog; +using Bogus; +using Participant; + +namespace ParticipantGrpc; + +internal class Participant +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomGrpcClient("http://127.0.0.1:5000"); + var participant = new ParticipantLogic(client); + + try + { + participant.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ParticipantGrpc/ParticipantGrpc.csproj b/Lab4/ParticipantGrpc/ParticipantGrpc.csproj new file mode 100644 index 0000000..bdbcc8a --- /dev/null +++ b/Lab4/ParticipantGrpc/ParticipantGrpc.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ParticipantRabbitMQ/Participant.cs b/Lab4/ParticipantRabbitMQ/Participant.cs new file mode 100644 index 0000000..d94e84c --- /dev/null +++ b/Lab4/ParticipantRabbitMQ/Participant.cs @@ -0,0 +1,60 @@ +using ChatRoomContract; +using NLog; +using Participant; + +namespace ParticipantRabbitMQ; + +internal class Participant +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + /// + /// Entry point + /// + static void Main() + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var chatRoom = new ChatRoomRabbitMQClient( + RabbitMQConfig.CreateConnection(), + RabbitMQConfig.ExchangeName, + RabbitMQConfig.CreateClientQueueName(), + RabbitMQConfig.ServerQueueName + ); + var participant = new ParticipantLogic(chatRoom); + + try + { + participant.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ParticipantRabbitMQ/ParticipantRabbitMQ.csproj b/Lab4/ParticipantRabbitMQ/ParticipantRabbitMQ.csproj new file mode 100644 index 0000000..bdbcc8a --- /dev/null +++ b/Lab4/ParticipantRabbitMQ/ParticipantRabbitMQ.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ParticipantRest/Participant.cs b/Lab4/ParticipantRest/Participant.cs new file mode 100644 index 0000000..35e239d --- /dev/null +++ b/Lab4/ParticipantRest/Participant.cs @@ -0,0 +1,52 @@ +using ChatRoomContract; +using NLog; +using Participant; + +namespace ParticipantRest; + +internal class Participant +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomRestClient("http://127.0.0.1:5000", new HttpClient()); + var participant = new ParticipantLogic(client); + + try + { + participant.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} diff --git a/Lab4/ParticipantRest/ParticipantRest.csproj b/Lab4/ParticipantRest/ParticipantRest.csproj new file mode 100644 index 0000000..bdbcc8a --- /dev/null +++ b/Lab4/ParticipantRest/ParticipantRest.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab4/ParticipantSimpleRPC/Participant.cs b/Lab4/ParticipantSimpleRPC/Participant.cs new file mode 100644 index 0000000..f1c9bae --- /dev/null +++ b/Lab4/ParticipantSimpleRPC/Participant.cs @@ -0,0 +1,81 @@ +using ChatRoomContract; +using Microsoft.Extensions.DependencyInjection; +using NLog; +using SimpleRpc.Transports.Http.Client; +using SimpleRpc.Serialization.Hyperion; +using SimpleRpc.Transports; +using System.Diagnostics; +using Participant; + +namespace ParticipantSimpleRPC; + +internal class Participant +{ + /// + /// Configures logging subsystem. + /// + private static 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; + } + + static void Main(string[] args) + { + Logger log = LogManager.GetCurrentClassLogger(); + + //configure logging + ConfigureLogging(); + + //initialize random number generator + var rnd = new Random(); + + while (true) + { + //connect to the server, get service client proxy + var sc = new ServiceCollection(); + sc + .AddSimpleRpcClient( + "chatRoomService", //must be same as on line 86 + new HttpClientTransportOptions + { + Url = "http://127.0.0.1:5001/simplerpc", + Serializer = "HyperionMessageSerializer" + } + ) + .AddSimpleRpcHyperionSerializer(); + + sc.AddSimpleRpcProxy("chatRoomService"); //must be same as on line 77 + + var sp = sc.BuildServiceProvider(); + + var chatRoom = sp.GetService(); + Debug.Assert(chatRoom != null); + + var participant = new ParticipantLogic(chatRoom); + + try + { + participant.Run(); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } +} + diff --git a/Lab4/ParticipantSimpleRPC/ParticipantSimpleRPC.csproj b/Lab4/ParticipantSimpleRPC/ParticipantSimpleRPC.csproj new file mode 100644 index 0000000..903a2e0 --- /dev/null +++ b/Lab4/ParticipantSimpleRPC/ParticipantSimpleRPC.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + +