From bc374acebaa354ddd7f4c57a9f0fd1a845bf133f Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 17 Oct 2024 22:55:51 +0300 Subject: [PATCH] complete lab2 grpc --- Lab1/Participant/Participant.cs | 8 + Lab2-grpc/.vscode/launch.json | 38 +++ Lab2-grpc/.vscode/tasks.json | 41 +++ Lab2-grpc/ChatRoom/ChatRoom.csproj | 18 ++ Lab2-grpc/ChatRoom/ChatRoomLogic.cs | 295 ++++++++++++++++++ Lab2-grpc/ChatRoom/ChatRoomService.cs | 110 +++++++ Lab2-grpc/ChatRoom/ChatRoomState.cs | 90 ++++++ Lab2-grpc/ChatRoom/Server.cs | 90 ++++++ Lab2-grpc/ChatRoom/appsettings.json | 11 + Lab2-grpc/ChatRoomContract/ChatRoomClient.cs | 114 +++++++ .../ChatRoomContract/ChatRoomContract.csproj | 23 ++ .../ChatRoomContract/IChatRoomService.cs | 74 +++++ .../Properties/launchSettings.json | 6 +- .../ChatRoomContract/Protos/service.proto | 61 ++++ Lab2-grpc/Lab2-grpc.sln | 42 +++ Lab2-grpc/Moderator/Moderator.cs | 92 ++++++ Lab2-grpc/Moderator/Moderator.csproj | 19 ++ Lab2-grpc/Participant/Participant.cs | 94 ++++++ Lab2-grpc/Participant/Participant.csproj | 19 ++ .../{Controllers => }/ChatRoomController.cs | 0 .../ChatRoom/Properties/launchSettings.json | 31 -- Lab2-rest/Participant/Participant.cs | 10 +- 22 files changed, 1251 insertions(+), 35 deletions(-) create mode 100644 Lab2-grpc/.vscode/launch.json create mode 100644 Lab2-grpc/.vscode/tasks.json create mode 100644 Lab2-grpc/ChatRoom/ChatRoom.csproj create mode 100644 Lab2-grpc/ChatRoom/ChatRoomLogic.cs create mode 100644 Lab2-grpc/ChatRoom/ChatRoomService.cs create mode 100644 Lab2-grpc/ChatRoom/ChatRoomState.cs create mode 100644 Lab2-grpc/ChatRoom/Server.cs create mode 100644 Lab2-grpc/ChatRoom/appsettings.json create mode 100644 Lab2-grpc/ChatRoomContract/ChatRoomClient.cs create mode 100644 Lab2-grpc/ChatRoomContract/ChatRoomContract.csproj create mode 100644 Lab2-grpc/ChatRoomContract/IChatRoomService.cs rename {Lab1/ChatRoom => Lab2-grpc/ChatRoomContract}/Properties/launchSettings.json (63%) create mode 100644 Lab2-grpc/ChatRoomContract/Protos/service.proto create mode 100644 Lab2-grpc/Lab2-grpc.sln create mode 100644 Lab2-grpc/Moderator/Moderator.cs create mode 100644 Lab2-grpc/Moderator/Moderator.csproj create mode 100644 Lab2-grpc/Participant/Participant.cs create mode 100644 Lab2-grpc/Participant/Participant.csproj rename Lab2-rest/ChatRoom/{Controllers => }/ChatRoomController.cs (100%) delete mode 100644 Lab2-rest/ChatRoom/Properties/launchSettings.json diff --git a/Lab1/Participant/Participant.cs b/Lab1/Participant/Participant.cs index e9f370f..d6ef14b 100644 --- a/Lab1/Participant/Participant.cs +++ b/Lab1/Participant/Participant.cs @@ -54,6 +54,14 @@ internal class Participant 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/Lab2-grpc/.vscode/launch.json b/Lab2-grpc/.vscode/launch.json new file mode 100644 index 0000000..54b90be --- /dev/null +++ b/Lab2-grpc/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ChatRoom", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ChatRoom", + "program": "${workspaceFolder}/ChatRoom/bin/Debug/net6.0/ChatRoom.dll", + "args": [], + "cwd": "${workspaceFolder}/ChatRoom", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Moderator", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Moderator", + "program": "${workspaceFolder}/Moderator/bin/Debug/net6.0/Moderator.dll", + "args": [], + "cwd": "${workspaceFolder}/Moderator", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Participant", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Participant", + "program": "${workspaceFolder}/Participant/bin/Debug/net6.0/Participant.dll", + "args": [], + "cwd": "${workspaceFolder}/Participant", + "console": "externalTerminal", + "stopAtEntry": false + } + ] +} \ No newline at end of file diff --git a/Lab2-grpc/.vscode/tasks.json b/Lab2-grpc/.vscode/tasks.json new file mode 100644 index 0000000..cf312c1 --- /dev/null +++ b/Lab2-grpc/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-ChatRoom", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ChatRoom/ChatRoom.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Moderator", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Moderator/Moderator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Participant", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Participant/Participant.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Lab2-grpc/ChatRoom/ChatRoom.csproj b/Lab2-grpc/ChatRoom/ChatRoom.csproj new file mode 100644 index 0000000..49be075 --- /dev/null +++ b/Lab2-grpc/ChatRoom/ChatRoom.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab2-grpc/ChatRoom/ChatRoomLogic.cs b/Lab2-grpc/ChatRoom/ChatRoomLogic.cs new file mode 100644 index 0000000..6481d92 --- /dev/null +++ b/Lab2-grpc/ChatRoom/ChatRoomLogic.cs @@ -0,0 +1,295 @@ +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/Lab2-grpc/ChatRoom/ChatRoomService.cs b/Lab2-grpc/ChatRoom/ChatRoomService.cs new file mode 100644 index 0000000..e02a6bf --- /dev/null +++ b/Lab2-grpc/ChatRoom/ChatRoomService.cs @@ -0,0 +1,110 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using ChatRoomContract.Protocol; + +namespace ChatRoom; + +public class ChatRoomService : ChatRoomContract.Protocol.ChatRoom.ChatRoomBase +{ + //NOTE: instance-per-request service would need logic to be static or injected from a singleton instance + private readonly ChatRoomLogic logic = new ChatRoomLogic(); + + /// + /// 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/Lab2-grpc/ChatRoom/ChatRoomState.cs b/Lab2-grpc/ChatRoom/ChatRoomState.cs new file mode 100644 index 0000000..4e6f3c2 --- /dev/null +++ b/Lab2-grpc/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/Lab2-grpc/ChatRoom/Server.cs b/Lab2-grpc/ChatRoom/Server.cs new file mode 100644 index 0000000..eb9e729 --- /dev/null +++ b/Lab2-grpc/ChatRoom/Server.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Server.Kestrel.Core; +using NLog; +using System.Net; + +namespace ChatRoom; + +public class Server +{ + public static void Main(string[] args) + { + var self = new Server(); + self.Run(args); + } + + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + /// + /// 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) + { + //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 support for GRPC services + builder.Services.AddGrpc(); + + //add the actual services + builder.Services.AddSingleton(new ChatRoomService()); + + //build the server + var app = builder.Build(); + + //turn on request routing + app.UseRouting(); + + //configure routes + app.MapGrpcService(); + + //run the server + app.Run(); + // app.RunAsync(); //use this if you need to implement background processing in the main thread + } +} \ No newline at end of file diff --git a/Lab2-grpc/ChatRoom/appsettings.json b/Lab2-grpc/ChatRoom/appsettings.json new file mode 100644 index 0000000..ae459eb --- /dev/null +++ b/Lab2-grpc/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/Lab2-grpc/ChatRoomContract/ChatRoomClient.cs b/Lab2-grpc/ChatRoomContract/ChatRoomClient.cs new file mode 100644 index 0000000..975684d --- /dev/null +++ b/Lab2-grpc/ChatRoomContract/ChatRoomClient.cs @@ -0,0 +1,114 @@ + +using ChatRoomContract.Protocol; +using Google.Protobuf.WellKnownTypes; +using Grpc.Net.Client; + +namespace ChatRoomContract; + +public class ChatRoomClient : IChatRoomService +{ + GrpcChannel channel; + ChatRoom.ChatRoomClient client; + + public ChatRoomClient(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/Lab2-grpc/ChatRoomContract/ChatRoomContract.csproj b/Lab2-grpc/ChatRoomContract/ChatRoomContract.csproj new file mode 100644 index 0000000..e92fe2f --- /dev/null +++ b/Lab2-grpc/ChatRoomContract/ChatRoomContract.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + Library + + + + + + + + + + + + + + + + diff --git a/Lab2-grpc/ChatRoomContract/IChatRoomService.cs b/Lab2-grpc/ChatRoomContract/IChatRoomService.cs new file mode 100644 index 0000000..1e57a5b --- /dev/null +++ b/Lab2-grpc/ChatRoomContract/IChatRoomService.cs @@ -0,0 +1,74 @@ +namespace ChatRoomContract; + +/// +/// Minimal message description +/// +public class Message +{ + /// + /// Message ID + /// + public int id; + /// + /// Message contents + /// + public string contents; + /// + /// Does this message need to be censored? + /// + 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/Lab1/ChatRoom/Properties/launchSettings.json b/Lab2-grpc/ChatRoomContract/Properties/launchSettings.json similarity index 63% rename from Lab1/ChatRoom/Properties/launchSettings.json rename to Lab2-grpc/ChatRoomContract/Properties/launchSettings.json index 58b5c8d..b6c3124 100644 --- a/Lab1/ChatRoom/Properties/launchSettings.json +++ b/Lab2-grpc/ChatRoomContract/Properties/launchSettings.json @@ -1,12 +1,12 @@ -{ +{ "profiles": { - "ChatRoom": { + "ChatRoomContract": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:54058;http://localhost:54059" + "applicationUrl": "https://localhost:51617;http://localhost:51618" } } } \ No newline at end of file diff --git a/Lab2-grpc/ChatRoomContract/Protos/service.proto b/Lab2-grpc/ChatRoomContract/Protos/service.proto new file mode 100644 index 0000000..66c05f8 --- /dev/null +++ b/Lab2-grpc/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/Lab2-grpc/Lab2-grpc.sln b/Lab2-grpc/Lab2-grpc.sln new file mode 100644 index 0000000..fa2fa82 --- /dev/null +++ b/Lab2-grpc/Lab2-grpc.sln @@ -0,0 +1,42 @@ + +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", "{CC3F5D4A-0D02-49FC-86BC-2FB160BE9F19}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{0DFEA55B-6A79-4874-A9A3-A08FA9FC30DE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moderator", "Moderator\Moderator.csproj", "{1D689F6B-33EF-49F7-B7A7-2F9D0840BFAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Participant", "Participant\Participant.csproj", "{62E8635E-9105-4764-B54E-D4968EEC4EA0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CC3F5D4A-0D02-49FC-86BC-2FB160BE9F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC3F5D4A-0D02-49FC-86BC-2FB160BE9F19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC3F5D4A-0D02-49FC-86BC-2FB160BE9F19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC3F5D4A-0D02-49FC-86BC-2FB160BE9F19}.Release|Any CPU.Build.0 = Release|Any CPU + {0DFEA55B-6A79-4874-A9A3-A08FA9FC30DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DFEA55B-6A79-4874-A9A3-A08FA9FC30DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DFEA55B-6A79-4874-A9A3-A08FA9FC30DE}.Release|Any CPU.Build.0 = Release|Any CPU + {1D689F6B-33EF-49F7-B7A7-2F9D0840BFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D689F6B-33EF-49F7-B7A7-2F9D0840BFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D689F6B-33EF-49F7-B7A7-2F9D0840BFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D689F6B-33EF-49F7-B7A7-2F9D0840BFAD}.Release|Any CPU.Build.0 = Release|Any CPU + {62E8635E-9105-4764-B54E-D4968EEC4EA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62E8635E-9105-4764-B54E-D4968EEC4EA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62E8635E-9105-4764-B54E-D4968EEC4EA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62E8635E-9105-4764-B54E-D4968EEC4EA0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B8CFA0A9-EAF7-4147-8F61-FD8375D77828} + EndGlobalSection +EndGlobal diff --git a/Lab2-grpc/Moderator/Moderator.cs b/Lab2-grpc/Moderator/Moderator.cs new file mode 100644 index 0000000..feeeb0d --- /dev/null +++ b/Lab2-grpc/Moderator/Moderator.cs @@ -0,0 +1,92 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Moderator; + +internal class Moderator +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + Console.Title = $"Moderator | {name} | {clientId}"; + + while (true) + { + var message = chatRoom.GetNewMessage(); + if (message != null) + { + log.Info($"Checking message ({message.id}): {message.contents}"); + Thread.Sleep(500); + + if (message.needsToBeCensored) + { + chatRoom.RejectMessage(message.id); + } + else + { + chatRoom.ApproveMessage(message.id); + } + } + + Thread.Sleep(1 * 1000); + } + } + + private void Run() + { + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomClient("http://127.0.0.1:5000"); + + try + { + RunConnection(client); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + static void Main(string[] args) + { + var self = new Moderator(); + self.Run(); + } +} diff --git a/Lab2-grpc/Moderator/Moderator.csproj b/Lab2-grpc/Moderator/Moderator.csproj new file mode 100644 index 0000000..ac175f5 --- /dev/null +++ b/Lab2-grpc/Moderator/Moderator.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab2-grpc/Participant/Participant.cs b/Lab2-grpc/Participant/Participant.cs new file mode 100644 index 0000000..f478d86 --- /dev/null +++ b/Lab2-grpc/Participant/Participant.cs @@ -0,0 +1,94 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Participant; + +internal class Participant +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + var rnd = new Random(); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + while (true) + { + int strikes = chatRoom.GetStrikes(clientId); + Console.Title = $"Participant | {name} | {clientId} | {strikes} Strikes"; + + var message = string.Join(" ", faker.Lorem.Words(5)); + bool needsToBeCensored = rnd.Next(0, 100) > 50; + if (chatRoom.SendMessage(clientId, message, needsToBeCensored)) { + log.Info("Sent message"); + } else { + log.Info("Failed to send message, blocked"); + var blockedUntil = chatRoom.GetBlockedUntil(clientId); + var now = DateTime.UtcNow; + if (blockedUntil != null && blockedUntil > now) + { + var delta = blockedUntil.Value - now; + log.Info($"Waiting {delta.TotalSeconds:F3}s until block expires"); + Thread.Sleep((int)delta.TotalMilliseconds); + } + } + + Thread.Sleep(2 * 1000); + } + } + + private void Run() + { + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomClient("http://127.0.0.1:5000"); + + try + { + RunConnection(client); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + static void Main(string[] args) + { + var self = new Participant(); + self.Run(); + } +} diff --git a/Lab2-grpc/Participant/Participant.csproj b/Lab2-grpc/Participant/Participant.csproj new file mode 100644 index 0000000..ac175f5 --- /dev/null +++ b/Lab2-grpc/Participant/Participant.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab2-rest/ChatRoom/Controllers/ChatRoomController.cs b/Lab2-rest/ChatRoom/ChatRoomController.cs similarity index 100% rename from Lab2-rest/ChatRoom/Controllers/ChatRoomController.cs rename to Lab2-rest/ChatRoom/ChatRoomController.cs diff --git a/Lab2-rest/ChatRoom/Properties/launchSettings.json b/Lab2-rest/ChatRoom/Properties/launchSettings.json deleted file mode 100644 index c20165d..0000000 --- a/Lab2-rest/ChatRoom/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:64988", - "sslPort": 0 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5125", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Lab2-rest/Participant/Participant.cs b/Lab2-rest/Participant/Participant.cs index 870d5d5..b7946bc 100644 --- a/Lab2-rest/Participant/Participant.cs +++ b/Lab2-rest/Participant/Participant.cs @@ -49,6 +49,14 @@ internal class Participant 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); @@ -62,7 +70,7 @@ internal class Participant while (true) { var client = new ChatRoomClient("http://127.0.0.1:5000", new HttpClient()); - + try { RunConnection(client);