complete lab3
This commit is contained in:
parent
a3134aa64f
commit
c88b6ac234
38
Lab3/.vscode/launch.json
vendored
Normal file
38
Lab3/.vscode/launch.json
vendored
Normal 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
41
Lab3/.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
Lab3/ChatRoom/ChatRoom.csproj
Normal file
20
Lab3/ChatRoom/ChatRoom.csproj
Normal 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>
|
296
Lab3/ChatRoom/ChatRoomLogic.cs
Normal file
296
Lab3/ChatRoom/ChatRoomLogic.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
156
Lab3/ChatRoom/ChatRoomService.cs
Normal file
156
Lab3/ChatRoom/ChatRoomService.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
Lab3/ChatRoom/ChatRoomState.cs
Normal file
97
Lab3/ChatRoom/ChatRoomState.cs
Normal 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
73
Lab3/ChatRoom/Server.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
241
Lab3/ChatRoomContract/ChatRoomClient.cs
Normal file
241
Lab3/ChatRoomContract/ChatRoomClient.cs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
Lab3/ChatRoomContract/ChatRoomContract.csproj
Normal file
14
Lab3/ChatRoomContract/ChatRoomContract.csproj
Normal 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>
|
30
Lab3/ChatRoomContract/Config.cs
Normal file
30
Lab3/ChatRoomContract/Config.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
81
Lab3/ChatRoomContract/IChatRoomService.cs
Normal file
81
Lab3/ChatRoomContract/IChatRoomService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
49
Lab3/ChatRoomContract/RPCMessage.cs
Normal file
49
Lab3/ChatRoomContract/RPCMessage.cs
Normal 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
43
Lab3/Lab3.sln
Normal 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
102
Lab3/Moderator/Moderator.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
19
Lab3/Moderator/Moderator.csproj
Normal file
19
Lab3/Moderator/Moderator.csproj
Normal 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>
|
107
Lab3/Participant/Participant.cs
Normal file
107
Lab3/Participant/Participant.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
19
Lab3/Participant/Participant.csproj
Normal file
19
Lab3/Participant/Participant.csproj
Normal 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>
|
Loading…
Reference in New Issue
Block a user