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);