1
0

complete lab2 grpc

This commit is contained in:
Rokas Puzonas 2024-10-17 22:55:51 +03:00
parent 4d2e40282d
commit bc374aceba
22 changed files with 1251 additions and 35 deletions

View File

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

38
Lab2-grpc/.vscode/launch.json vendored Normal file
View File

@ -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
}
]
}

41
Lab2-grpc/.vscode/tasks.json vendored Normal file
View File

@ -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"
}
]
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.66.0" />
<PackageReference Include="NLog" Version="5.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,295 @@
using NLog;
namespace ChatRoom;
/// <summary>
/// Chat room service logic
/// </summary>
public class ChatRoomLogic
{
/// <summary>
/// Background thread of deleting approved or rejected messages
/// </summary>
private Thread thread;
/// <summary>
/// Chat Room state
/// </summary>
private ChatRoomState state = new ChatRoomState();
/// <summary>
/// Logger
/// </summary>
private Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Chat room logic constructor
/// </summary>
public ChatRoomLogic()
{
thread = new Thread(BackgroundTask);
thread.Start();
}
/// <summary>
/// Generate the next incrementing ID
/// </summary>
/// <returns>Unique ID</returns>
int NextId()
{
int id = state.lastUniqueId;
state.lastUniqueId++;
return id;
}
/// <summary>
/// Register a client
/// </summary>
/// <param name="name">Client name</param>
/// <returns>Client ID</returns>
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;
}
}
/// <summary>
/// Find a client by ID, can return NULL if not found
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Optional client object</returns>
Client? FindClientById(int clientId)
{
foreach (var client in state.clients)
{
if (client.id == clientId)
{
return client;
}
}
return null;
}
/// <summary>
/// Find message by ID, can return NULL if not found
/// </summary>
/// <param name="messageId">Message ID</param>
/// <returns>Optional message object</returns>
Message? FindMessageById(int messageId)
{
foreach (var message in state.messages)
{
if (message.id == messageId)
{
return message;
}
}
return null;
}
/// <summary>
/// Check if client is still blocked, will clear `blockedUntil` if client became unblocked
/// </summary>
/// <param name="client">Client object</param>
/// <returns>Is client blocked?</returns>
bool GetAndUpdateBlockedState(Client client)
{
if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil)
{
client.blockedUntil = null;
}
return client.blockedUntil != null;
}
/// <summary>
/// Send a message
/// </summary>
/// <param name="clientId">Client ID</param>
/// <param name="contents">Message contents</param>
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
/// <returns>Was sending the message successful, can fail if client is blocked</returns>
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;
}
/// <summary>
/// Get next message which isin't approved or rejected
/// </summary>
/// <returns>Optional message object</returns>
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;
}
/// <summary>
/// Approve a message
/// </summary>
/// <param name="messageId">Message ID</param>
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");
}
}
/// <summary>
/// Reject a message
/// </summary>
/// <param name="messageId">Message ID</param>
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;
}
}
}
}
/// <summary>
/// Get number of strikes a client has
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Number of strikes</returns>
public int GetStrikes(int clientId)
{
lock (state.accessLock)
{
var client = FindClientById(clientId);
if (client == null)
{
return 0;
}
return client.strikes;
}
}
/// <summary>
/// Get timestamp until when the client is blocked
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Optional datetime object</returns>
public DateTime? GetBlockedUntil(int clientId)
{
lock (state.accessLock)
{
var client = FindClientById(clientId);
if (client == null)
{
return null;
}
return client.blockedUntil;
}
}
/// <summary>
/// Main loop for background thread. Used for deleting approved or rejected messages.
/// </summary>
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);
}
}
}

View File

@ -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();
/// <summary>
/// Register client with a name
/// </summary>
/// <param name="input">Name of client.</param>
/// <param name="context">Call context.</param>
/// <returns>Client ID</returns>
public override Task<ClientId> RegisterClient(RegisterClientRequest request, ServerCallContext context)
{
var result = new ClientId { Id = logic.RegisterClient(request.Name) };
return Task.FromResult(result);
}
/// <summary>
/// Get number of strikes a participant has
/// </summary>
/// <param name="input">Client ID</param>
/// <param name="context">Call context.</param>
/// <returns>Number of strikes</returns>
public override Task<Srikes> GetStrikes(ClientId request, ServerCallContext context)
{
var result = new Srikes { Strikes = logic.GetStrikes(request.Id) };
return Task.FromResult(result);
}
/// <summary>
/// Get timestamp until when the client is blocked
/// </summary>
/// <param name="input">Client ID</param>
/// <param name="context">Call context.</param>
/// <returns>Optional datetime object</returns>
public override Task<BlockedUntil> 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);
}
/// <summary>
/// Send a message, will be given to a moderator to be approved
/// </summary>
/// <param name="input">Message details</param>
/// <param name="context">Call context.</param>
/// <returns>Was sending successful, can fail if user is blocked</returns>
public override Task<BoolResponse> 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);
}
/// <summary>
/// Get the next message which hasn't been approved or rejected
/// </summary>
/// <param name="input">Empty</param>
/// <param name="context">Call context.</param>
/// <returns>Message object. Returns null if there is no message</returns>
public override Task<NewUserMessage> 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);
}
/// <summary>
/// Reject a message
/// </summary>
/// <param name="input">Message ID</param>
/// <param name="context">Call context.</param>
/// <returns>Empty</returns>
public override Task<Empty> ApproveMessage(MessageId request, ServerCallContext context)
{
logic.ApproveMessage(request.Id);
return Task.FromResult(new Empty());
}
/// <summary>
/// Approve a message
/// </summary>
/// <param name="input">Message ID</param>
/// <param name="context">Call context.</param>
/// <returns>Empty</returns>
public override Task<Empty> RejectMessage(MessageId request, ServerCallContext context)
{
logic.RejectMessage(request.Id);
return Task.FromResult(new Empty());
}
}

View File

@ -0,0 +1,90 @@
namespace ChatRoom;
/// <summary>
/// Client information
/// </summary>
public class Client
{
/// <summary>
/// Client ID
/// </summary>
public int id;
/// <summary>
/// Client name, can be the same between multiple clients
/// </summary>
public string name;
/// <summary>
/// Number of strikes
/// </summary>
public int strikes = 0;
/// <summary>
/// Until when is this client blocked from sending messages
/// </summary>
public DateTime? blockedUntil = null;
}
/// <summary>
/// Describes the messages status/stage
/// </summary>
public enum MessageStatus
{
WaitingForModerator,
GivenToModerator,
Approved,
Rejected
}
/// <summary>
/// Message information
/// </summary>
public class Message
{
/// <summary>
/// Message ID
/// </summary>
public int id;
/// <summary>
/// Client ID
/// </summary>
public int clientId;
/// <summary>
/// Message contents
/// </summary>
public string contents;
/// <summary>
/// Does this message need to be censored?
/// </summary>
public bool needsToBeCensored;
/// <summary>
/// Message status/stage
/// </summary>
public MessageStatus status = MessageStatus.WaitingForModerator;
/// <summary>
/// When was this message sent
/// </summary>
public DateTime sentAt;
}
public class ChatRoomState
{
/// <summary>
/// Access lock.
/// </summary>
public readonly object accessLock = new object();
/// <summary>
/// Last unique ID value generated.
/// </summary>
public int lastUniqueId;
/// <summary>
/// List of all registered clients
/// </summary>
public List<Client> clients = new List<Client>();
/// <summary>
/// List of messages
/// </summary>
public List<Message> messages = new List<Message>();
}

View File

@ -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);
}
/// <summary>
/// Logger for this class.
/// </summary>
Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Configures logging subsystem.
/// </summary>
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;
}
/// <summary>
/// Program body.
/// </summary>
/// <param name="args">Command line arguments.</param>
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);
}
/// <summary>
/// Starts integrated server.
/// </summary>
/// <param name="args">Command line arguments.</param>
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<ChatRoomService>();
//run the server
app.Run();
// app.RunAsync(); //use this if you need to implement background processing in the main thread
}
}

View File

@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -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);
}
/// <summary>
/// Approve a message
/// </summary>
/// <param name="messageId">Message ID</param>
public void ApproveMessage(int messageId)
{
client.ApproveMessage(new MessageId { Id = messageId });
}
/// <summary>
/// Get timestamp until when the client is blocked
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Optional datetime object</returns>
public DateTime? GetBlockedUntil(int clientId)
{
var result = client.GetBlockedUntil(new ClientId { Id = clientId });
if (result.HasTimestamp)
{
return result.Timestamp.ToDateTime();
} else
{
return null;
}
}
/// <summary>
/// Get the next message which hasn't been approved or rejected
/// </summary>
/// <returns>Message object. Returns null if there is no message</returns>
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;
}
}
/// <summary>
/// Get number of strikes a participant has
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Number of strikes</returns>
public int GetStrikes(int clientId)
{
var result = client.GetStrikes(new ClientId { Id = clientId });
return result.Strikes;
}
/// <summary>
/// Register client with a name
/// </summary>
/// <param name="name">Name of client, can be duplicate between clients</param>
/// <returns>Client ID</returns>
public int RegisterClient(string name)
{
var result = client.RegisterClient(new RegisterClientRequest { Name = name });
return result.Id;
}
/// <summary>
/// Reject a message
/// </summary>
/// <param name="messageId">Message ID</param>
public void RejectMessage(int messageId)
{
client.RejectMessage(new MessageId { Id = messageId });
}
/// <summary>
/// Send a message, will be given to a moderator to be approved
/// </summary>
/// <param name="clientId">Client ID</param>
/// <param name="contents">Message contents</param>
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
/// <returns>Was sending successful, can fail if user is blocked</returns>
public bool SendMessage(int clientId, string contents, bool needsToBeCensored)
{
var result = client.SendMessage(new UserMessageRequest
{
ClientId = clientId,
Contents = contents,
NeedsToBeCensored = needsToBeCensored
});
return result.Success;
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<None Remove="Protos\service.proto" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\service.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.66.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,74 @@
namespace ChatRoomContract;
/// <summary>
/// Minimal message description
/// </summary>
public class Message
{
/// <summary>
/// Message ID
/// </summary>
public int id;
/// <summary>
/// Message contents
/// </summary>
public string contents;
/// <summary>
/// Does this message need to be censored?
/// </summary>
public bool needsToBeCensored;
}
/// <summary>
/// Chat room service contract
/// </summary>
public interface IChatRoomService
{
/// <summary>
/// Register client with a name
/// </summary>
/// <param name="name">Name of client, can be duplicate between clients</param>
/// <returns>Client ID</returns>
int RegisterClient(string name);
/// <summary>
/// Get number of strikes a participant has
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Number of strikes</returns>
int GetStrikes(int clientId);
/// <summary>
/// Get timestamp until when the client is blocked
/// </summary>
/// <param name="clientId">Client ID</param>
/// <returns>Optional datetime object</returns>
DateTime? GetBlockedUntil(int clientId);
/// <summary>
/// Send a message, will be given to a moderator to be approved
/// </summary>
/// <param name="clientId">Client ID</param>
/// <param name="contents">Message contents</param>
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
/// <returns>Was sending successful, can fail if user is blocked</returns>
bool SendMessage(int clientId, string contents, bool needsToBeCensored);
/// <summary>
/// Get the next message which hasn't been approved or rejected
/// </summary>
/// <returns>Message object. Returns null if there is no message</returns>
Message? GetNewMessage();
/// <summary>
/// Reject a message
/// </summary>
/// <param name="messageId">Message ID</param>
void RejectMessage(int messageId);
/// <summary>
/// Approve a message
/// </summary>
/// <param name="messageId">Message ID</param>
void ApproveMessage(int messageId);
}

View File

@ -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"
}
}
}

View File

@ -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;
}

42
Lab2-grpc/Lab2-grpc.sln Normal file
View File

@ -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

View File

@ -0,0 +1,92 @@
using ChatRoomContract;
using NLog;
using Bogus;
namespace Moderator;
internal class Moderator
{
/// <summary>
/// Logger for this class.
/// </summary>
Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Configures logging subsystem.
/// </summary>
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();
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="NLog" Version="5.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,94 @@
using ChatRoomContract;
using NLog;
using Bogus;
namespace Participant;
internal class Participant
{
/// <summary>
/// Logger for this class.
/// </summary>
Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Configures logging subsystem.
/// </summary>
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();
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="NLog" Version="5.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
</ItemGroup>
</Project>

View File

@ -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"
}
}
}
}

View File

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