1
0

complete lab3

This commit is contained in:
Rokas Puzonas 2024-11-16 16:44:29 +02:00
parent a3134aa64f
commit c88b6ac234
17 changed files with 1426 additions and 0 deletions

38
Lab3/.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
Lab3/.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,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,296 @@
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,156 @@
using RabbitMQ.Client;
using Newtonsoft.Json;
using RabbitMQ.Client.Events;
using ChatRoomContract;
using System.Text;
using MessagePack;
using System.Diagnostics;
using NLog.LayoutRenderers.Wrappers;
using NLog;
namespace ChatRoom;
internal class ChatRoomService
{
/// <summary>
/// Logger for this class.
/// </summary>
private Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Communications channel to RabbitMQ message broker.
/// </summary>
private IModel rmqChannel;
/// <summary>
/// Service logic.
/// </summary>
private ChatRoomLogic logic = new ChatRoomLogic();
public ChatRoomService(IConnection connection, string exchangeName, string serverQueueName)
{
//get channel, configure exchanges and request queue
rmqChannel = connection.CreateModel();
rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
rmqChannel.QueueDeclare(queue: serverQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
rmqChannel.QueueBind(queue: serverQueueName, exchange: exchangeName, routingKey: serverQueueName, arguments: null);
//connect to the queue as consumer
//XXX: see https://www.rabbitmq.com/dotnet-api-guide.html#concurrency for threading issues
var rmqConsumer = new EventingBasicConsumer(rmqChannel);
rmqConsumer.Received += (consumer, delivery) => OnMessageReceived(((EventingBasicConsumer)consumer).Model, delivery);
rmqChannel.BasicConsume(queue: serverQueueName, autoAck: true, consumer: rmqConsumer);
}
/// <summary>
/// Is invoked to process messages received.
/// </summary>
/// <param name="channel">Related communications channel.</param>
/// <param name="msgIn">Message deliver data.</param>
private void OnMessageReceived(IModel channel, BasicDeliverEventArgs msgIn)
{
try
{
var msg = MessagePackSerializer.Deserialize<RPCMessage>(msgIn.Body);
Debug.Assert(msg != null);
if (msg.isResponse == true)
{
return;
}
bool hasResponse = false;
byte[]? response = null;
switch (msg.method)
{
case nameof(IChatRoomService.RegisterClient):
{
var name = MessagePackSerializer.Deserialize<string>(msg.args);
var clientId = logic.RegisterClient(name);
response = MessagePackSerializer.Serialize(clientId);
hasResponse = true;
break;
}
case nameof(IChatRoomService.GetStrikes):
{
var clientId = MessagePackSerializer.Deserialize<int>(msg.args);
var strikes = logic.GetStrikes(clientId);
response = MessagePackSerializer.Serialize(strikes);
hasResponse = true;
break;
}
case nameof(IChatRoomService.GetBlockedUntil):
{
var clientId = MessagePackSerializer.Deserialize<int>(msg.args);
var blockedUntil = logic.GetBlockedUntil(clientId);
response = MessagePackSerializer.Serialize(blockedUntil);
hasResponse = true;
break;
}
case nameof(IChatRoomService.SendMessage):
{
var args = MessagePackSerializer.Deserialize<SendMessageArgs>(msg.args);
var success = logic.SendMessage(args.clientId, args.contents, args.needsToBeCensored);
response = MessagePackSerializer.Serialize(success);
hasResponse = true;
break;
}
case nameof(IChatRoomService.GetNewMessage):
{
var newMessage = logic.GetNewMessage();
response = MessagePackSerializer.Serialize(newMessage);
hasResponse = true;
break;
}
case nameof(IChatRoomService.RejectMessage):
{
var messageId = MessagePackSerializer.Deserialize<int>(msg.args);
logic.ApproveMessage(messageId);
break;
}
case nameof(IChatRoomService.ApproveMessage):
{
var messageId = MessagePackSerializer.Deserialize<int>(msg.args);
logic.ApproveMessage(messageId);
break;
}
default:
{
throw new Exception("Unknown RPC method");
}
}
if (hasResponse)
{
var responseMsg = new RPCMessage
{
isResponse = true,
method = msg.method,
args = response
};
var properties = channel.CreateBasicProperties();
properties.CorrelationId = msgIn.BasicProperties.CorrelationId;
channel.BasicPublish(
exchange: msgIn.Exchange,
routingKey: msgIn.BasicProperties.ReplyTo,
basicProperties: properties,
body: MessagePackSerializer.Serialize(responseMsg)
);
}
} catch (Exception e)
{
log.Error(e, "Unhandled exception caught when processing a message. The message is now lost.");
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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>();
}

73
Lab3/ChatRoom/Server.cs Normal file
View File

@ -0,0 +1,73 @@
using ChatRoomContract;
using NLog;
namespace ChatRoom;
internal class Server
{
/// <summary>
/// Logger for this class.
/// </summary>
private Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Configure loggin 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>
private void Run()
{
//configure logging
ConfigureLogging();
while (true)
{
try
{
var service = new ChatRoomService(Config.CreateConnection(), Config.ExchangeName, Config.ServerQueueName);
log.Info("Server has been started.");
//hang main thread
while (true)
{
Thread.Sleep(1000);
}
}
catch (Exception e)
{
//log exception
log.Error(e, "Unhandled exception caught. Server will now restart.");
//prevent console spamming
Thread.Sleep(2000);
}
}
}
/// <summary>
/// Program entry point.
/// </summary>
/// <param name="args">Command line arguments.</param>
static void Main(string[] args)
{
var self = new Server();
self.Run();
}
}

View File

@ -0,0 +1,241 @@
using MessagePack;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace ChatRoomContract;
/// <summary>
/// RabbitMQ chat room client
/// </summary>
public class ChatRoomClient : IChatRoomService
{
/// <summary>
/// Exchange name
/// </summary>
private string exchangeName;
/// <summary>
/// Client queue name
/// </summary>
private string clientQueueName;
/// <summary>
/// Server queue name
/// </summary>
private string serverQueueName;
/// <summary>
/// RabbitMQ channel
/// </summary>
private IModel rmqChannel;
/// <summary>
/// Chat Room client constructor
/// </summary>
/// <param name="connection">RabbitMQ connection</param>
/// <param name="exchangeName">Exchange name</param>
/// <param name="clientQueueName">Client queue name</param>
/// <param name="serverQueueName">Server queue name</param>
public ChatRoomClient(IConnection connection, string exchangeName, string clientQueueName, string serverQueueName)
{
this.exchangeName = exchangeName;
this.clientQueueName = clientQueueName;
this.serverQueueName = serverQueueName;
rmqChannel = connection.CreateModel();
rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
rmqChannel.QueueDeclare(queue: clientQueueName, durable: false, exclusive: true, autoDelete: false, arguments: null);
rmqChannel.QueueBind(queue: clientQueueName, exchange: exchangeName, routingKey: clientQueueName, arguments: null);
}
/// <summary>
/// Send rpc message without waiting for a response
/// </summary>
/// <param name="method">Method name</param>
/// <param name="args">Serialized arguments</param>
/// <param name="correlationId">Optional correlation ID</param>
private void CallVoid(string method, byte[] args, string? correlationId = null)
{
correlationId ??= Guid.NewGuid().ToString();
var requestProps = rmqChannel.CreateBasicProperties();
requestProps.CorrelationId = correlationId;
requestProps.ReplyTo = clientQueueName;
var msg = new RPCMessage{
isResponse = false,
method = method,
args = args
};
rmqChannel.BasicPublish(
exchange: exchangeName,
routingKey: serverQueueName,
basicProperties: requestProps,
body: MessagePackSerializer.Serialize(msg)
);
}
/// <summary>
/// Send a rpc message and wait for a response
/// </summary>
/// <typeparam name="ResultType">Result type</typeparam>
/// <param name="method">Method name</param>
/// <param name="args">Serialized arguments</param>
/// <returns>Result</returns>
private ResultType Call<ResultType>(string method, byte[]? args)
{
var correlationId = Guid.NewGuid().ToString();
var isResultReady = false;
var resultReadySignal = new AutoResetEvent(false);
ResultType result = default;
//ensure contents of variables set in main thread, are loadable by receiver thread
Thread.MemoryBarrier();
//create response message consumer
var consumer = new EventingBasicConsumer(rmqChannel);
consumer.Received +=
(channel, delivery) => {
//ensure contents of variables set in main thread are loaded into this thread
Thread.MemoryBarrier();
if (isResultReady)
{
return;
}
if (delivery.BasicProperties.CorrelationId != correlationId)
{
return;
}
var msg = MessagePackSerializer.Deserialize<RPCMessage>(delivery.Body);
if (msg.isResponse && msg.method == method)
{
if (msg.args != null)
{
result = MessagePackSerializer.Deserialize<ResultType>(msg.args);
}
//indicate result has been received, ensure it is loadable by main thread
isResultReady = true;
Thread.MemoryBarrier();
//signal main thread that result has been received
resultReadySignal.Set();
}
};
//attach message consumer to the response queue
var consumerTag = rmqChannel.BasicConsume(clientQueueName, true, consumer);
CallVoid(method, args, correlationId);
//wait for the result to be ready
resultReadySignal.WaitOne();
//ensure contents of variables set by the receiver are loaded into this thread
Thread.MemoryBarrier();
//detach message consumer from the response queue
rmqChannel.BasicCancel(consumerTag);
return result;
}
/// <summary>
/// Approve a message
/// </summary>
/// <param name="messageId">Message ID</param>
public void ApproveMessage(int messageId)
{
CallVoid(
nameof(IChatRoomService.ApproveMessage),
MessagePackSerializer.Serialize(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)
{
return Call<DateTime?>(
nameof(IChatRoomService.GetBlockedUntil),
MessagePackSerializer.Serialize(clientId)
);
}
/// <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()
{
return Call<Message?>(
nameof(IChatRoomService.GetNewMessage),
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)
{
return Call<int>(
nameof(IChatRoomService.GetStrikes),
MessagePackSerializer.Serialize(clientId)
);
}
/// <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)
{
return Call<int>(
nameof(IChatRoomService.RegisterClient),
MessagePackSerializer.Serialize(name)
);
}
/// <summary>
/// Reject a message
/// </summary>
/// <param name="messageId">Message ID</param>
public void RejectMessage(int messageId)
{
CallVoid(
nameof(IChatRoomService.RejectMessage),
MessagePackSerializer.Serialize(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 args = new SendMessageArgs {
clientId = clientId,
contents = contents,
needsToBeCensored = needsToBeCensored
};
return Call<bool>(
nameof(IChatRoomService.SendMessage),
MessagePackSerializer.Serialize(args)
);
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.192" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,30 @@
using RabbitMQ.Client;
namespace ChatRoomContract;
public static class Config
{
public static string ExchangeName = "T120B180.ChatRoom.Exchange";
public static string ServerQueueName = "T120B180.ChatRoom.Server";
public static string ClientQueueNamePrefix = "T120B180.ChatRoom.Client_";
public static string HostName = "localhost";
public static string Username = "guest";
public static string Password = "guest";
public static string CreateClientQueueName()
{
var ClientId = Guid.NewGuid().ToString();
return ClientQueueNamePrefix + ClientId;
}
public static IConnection CreateConnection()
{
var rmqConnectionFactory = new ConnectionFactory
{
HostName = HostName,
UserName = Username,
Password = Password
};
return rmqConnectionFactory.CreateConnection();
}
}

View File

@ -0,0 +1,81 @@
using MessagePack;
namespace ChatRoomContract;
/// <summary>
/// Minimal message description
/// </summary>
[MessagePackObject]
public class Message
{
/// <summary>
/// Message ID
/// </summary>
[Key(0)]
public int id;
/// <summary>
/// Message contents
/// </summary>
[Key(1)]
public string contents;
/// <summary>
/// Does this message need to be censored?
/// </summary>
[Key(2)]
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

@ -0,0 +1,49 @@
using MessagePack;
namespace ChatRoomContract;
/// <summary>
/// RabbitMQ message
/// </summary>
[MessagePackObject]
public class RPCMessage
{
/// <summary>
/// Is this message a response
/// </summary>
[Key(0)]
public bool isResponse;
/// <summary>
/// Method name
/// </summary>
[Key(1)]
public string method;
/// <summary>
/// Optional arguments
/// </summary>
[Key(2)]
public byte[]? args;
}
/// <summary>
/// IChatRoomService.SendMessage arguments
/// </summary>
[MessagePackObject]
public class SendMessageArgs
{
/// <summary>
/// Client ID
/// </summary>
[Key(0)]
public int clientId { get; set; }
/// <summary>
/// Message contents
/// </summary>
[Key(1)]
public string contents { get; set; }
/// <summary>
/// Does this message need to be censored?
/// </summary>
[Key(2)]
public bool needsToBeCensored { get; set; }
}

43
Lab3/Lab3.sln Normal file
View File

@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35208.52
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoom", "ChatRoom\ChatRoom.csproj", "{BDE934D4-F8C9-484F-965F-FBBDE10818EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moderator", "Moderator\Moderator.csproj", "{CC693E3A-9508-4D87-8B5F-630DD268858F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Participant", "Participant\Participant.csproj", "{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.Build.0 = Release|Any CPU
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.Build.0 = Release|Any CPU
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B2EFC0A1-DB91-4736-B795-29ACE70DCBA4}
EndGlobalSection
EndGlobal

102
Lab3/Moderator/Moderator.cs Normal file
View File

@ -0,0 +1,102 @@
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;
}
/// <summary>
/// Run with a given service
/// </summary>
/// <param name="chatRoom">Chat room service</param>
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);
}
}
/// <summary>
/// Main loop
/// </summary>
private void Run()
{
ConfigureLogging();
while (true)
{
var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName);
try
{
RunConnection(chatRoom);
}
catch (Exception e)
{
//log whatever exception to console
log.Warn(e, "Unhandled exception caught. Will restart main loop.");
//prevent console spamming
Thread.Sleep(2000);
}
}
}
/// <summary>
/// Entry point
/// </summary>
static void Main()
{
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,107 @@
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;
}
/// <summary>
/// Run with a given service
/// </summary>
/// <param name="chatRoom">Chat room service</param>
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);
}
}
/// <summary>
/// Main loop
/// </summary>
private void Run()
{
ConfigureLogging();
while (true)
{
var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName);
try
{
RunConnection(chatRoom);
}
catch (Exception e)
{
//log whatever exception to console
log.Warn(e, "Unhandled exception caught. Will restart main loop.");
//prevent console spamming
Thread.Sleep(2000);
}
}
}
/// <summary>
/// Entry point
/// </summary>
static void Main()
{
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>