diff --git a/Lab2-rest/.vscode/launch.json b/Lab2-rest/.vscode/launch.json new file mode 100644 index 0000000..54b90be --- /dev/null +++ b/Lab2-rest/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ChatRoom", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ChatRoom", + "program": "${workspaceFolder}/ChatRoom/bin/Debug/net6.0/ChatRoom.dll", + "args": [], + "cwd": "${workspaceFolder}/ChatRoom", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Moderator", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Moderator", + "program": "${workspaceFolder}/Moderator/bin/Debug/net6.0/Moderator.dll", + "args": [], + "cwd": "${workspaceFolder}/Moderator", + "console": "externalTerminal", + "stopAtEntry": false + }, + { + "name": "Participant", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-Participant", + "program": "${workspaceFolder}/Participant/bin/Debug/net6.0/Participant.dll", + "args": [], + "cwd": "${workspaceFolder}/Participant", + "console": "externalTerminal", + "stopAtEntry": false + } + ] +} \ No newline at end of file diff --git a/Lab2-rest/.vscode/tasks.json b/Lab2-rest/.vscode/tasks.json new file mode 100644 index 0000000..cf312c1 --- /dev/null +++ b/Lab2-rest/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-ChatRoom", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ChatRoom/ChatRoom.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Moderator", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Moderator/Moderator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-Participant", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Participant/Participant.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Lab2-rest/ChatRoom/ChatRoom.csproj b/Lab2-rest/ChatRoom/ChatRoom.csproj new file mode 100644 index 0000000..2127933 --- /dev/null +++ b/Lab2-rest/ChatRoom/ChatRoom.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + True + + + + + + + + + + + + diff --git a/Lab2-rest/ChatRoom/ChatRoomLogic.cs b/Lab2-rest/ChatRoom/ChatRoomLogic.cs new file mode 100644 index 0000000..6481d92 --- /dev/null +++ b/Lab2-rest/ChatRoom/ChatRoomLogic.cs @@ -0,0 +1,295 @@ +using NLog; + +namespace ChatRoom; + +/// +/// Chat room service logic +/// +public class ChatRoomLogic +{ + /// + /// Background thread of deleting approved or rejected messages + /// + private Thread thread; + /// + /// Chat Room state + /// + private ChatRoomState state = new ChatRoomState(); + /// + /// Logger + /// + private Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Chat room logic constructor + /// + public ChatRoomLogic() + { + thread = new Thread(BackgroundTask); + thread.Start(); + } + + /// + /// Generate the next incrementing ID + /// + /// Unique ID + int NextId() + { + int id = state.lastUniqueId; + state.lastUniqueId++; + return id; + } + + /// + /// Register a client + /// + /// Client name + /// Client ID + public int RegisterClient(string name) + { + lock (state.accessLock) + { + int clientId = NextId(); + state.clients.Add(new Client + { + id = clientId, + name = name + }); + log.Info($"Registered with client '{name}' with id {clientId}"); + return clientId; + } + } + + /// + /// Find a client by ID, can return NULL if not found + /// + /// Client ID + /// Optional client object + Client? FindClientById(int clientId) + { + foreach (var client in state.clients) + { + if (client.id == clientId) + { + return client; + } + } + return null; + } + + /// + /// Find message by ID, can return NULL if not found + /// + /// Message ID + /// Optional message object + Message? FindMessageById(int messageId) + { + foreach (var message in state.messages) + { + if (message.id == messageId) + { + return message; + } + } + return null; + } + + /// + /// Check if client is still blocked, will clear `blockedUntil` if client became unblocked + /// + /// Client object + /// Is client blocked? + bool GetAndUpdateBlockedState(Client client) + { + if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil) + { + client.blockedUntil = null; + } + + return client.blockedUntil != null; + } + + /// + /// Send a message + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending the message successful, can fail if client is blocked + public bool SendMessage(int clientId, string contents, bool needsToBeCensored) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return false; + } + + if (GetAndUpdateBlockedState(client)) + { + return false; + } + + var message = new Message + { + id = NextId(), + clientId = clientId, + contents = contents, + needsToBeCensored = needsToBeCensored, + status = MessageStatus.WaitingForModerator, + sentAt = DateTime.UtcNow + }; + state.messages.Add(message); + log.Info($"Client '{client.name}' ({client.id}) sent message '{contents}' ({message.id}). Needs to censored: {needsToBeCensored}"); + } + + return true; + } + + /// + /// Get next message which isin't approved or rejected + /// + /// Optional message object + public ChatRoomContract.Message? GetNewMessage() + { + lock (state.accessLock) + { + foreach (var message in state.messages) + { + if (message.status != MessageStatus.WaitingForModerator) continue; + + log.Info($"Message '{message.id}' given to moderator"); + message.status = MessageStatus.GivenToModerator; + return new ChatRoomContract.Message{ + id = message.id, + contents = message.contents, + needsToBeCensored = message.needsToBeCensored + }; + } + } + + return null; + } + + /// + /// Approve a message + /// + /// Message ID + public void ApproveMessage(int messageId) + { + lock (state.accessLock) + { + var message = FindMessageById(messageId); + if (message == null) + { + return; + } + + if (message.status != MessageStatus.GivenToModerator) + { + return; + } + + message.status = MessageStatus.Approved; + log.Info($"Message {message.id} was approved"); + } + } + + /// + /// Reject a message + /// + /// Message ID + public void RejectMessage(int messageId) + { + lock (state.accessLock) + { + var message = FindMessageById(messageId); + if (message == null) + { + return; + } + + if (message.status != MessageStatus.GivenToModerator) + { + return; + } + + message.status = MessageStatus.Rejected; + log.Info($"Message {message.id} was rejected"); + + var client = FindClientById(message.clientId); + if (client != null && !GetAndUpdateBlockedState(client)) + { + client.strikes++; + + var rnd = new Random(); + if (client.strikes > rnd.Next(0, 10)) + { + log.Info($"Client '{client.name}' ({client.id}) was blocked for {client.strikes}s"); + client.blockedUntil = DateTime.UtcNow.AddSeconds(client.strikes); + client.strikes = 0; + } + } + } + } + + /// + /// Get number of strikes a client has + /// + /// Client ID + /// Number of strikes + public int GetStrikes(int clientId) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return 0; + } + + return client.strikes; + } + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + public DateTime? GetBlockedUntil(int clientId) + { + lock (state.accessLock) + { + var client = FindClientById(clientId); + if (client == null) + { + return null; + } + + return client.blockedUntil; + } + } + + /// + /// Main loop for background thread. Used for deleting approved or rejected messages. + /// + public void BackgroundTask() + { + while (true) + { + lock (state.accessLock) + { + var now = DateTime.UtcNow; + int count = state.messages.RemoveAll(msg => + (now - msg.sentAt).TotalSeconds > 5 && + (msg.status == MessageStatus.Approved || msg.status == MessageStatus.Rejected) + ); + log.Info($"Running periodic cleanup, removed {count} messages"); + } + + Thread.Sleep(10 * 1000); + } + } +} diff --git a/Lab2-rest/ChatRoom/ChatRoomState.cs b/Lab2-rest/ChatRoom/ChatRoomState.cs new file mode 100644 index 0000000..4e6f3c2 --- /dev/null +++ b/Lab2-rest/ChatRoom/ChatRoomState.cs @@ -0,0 +1,90 @@ +namespace ChatRoom; + +/// +/// Client information +/// +public class Client +{ + /// + /// Client ID + /// + public int id; + /// + /// Client name, can be the same between multiple clients + /// + public string name; + /// + /// Number of strikes + /// + public int strikes = 0; + /// + /// Until when is this client blocked from sending messages + /// + public DateTime? blockedUntil = null; +} + +/// +/// Describes the messages status/stage +/// +public enum MessageStatus +{ + WaitingForModerator, + GivenToModerator, + Approved, + Rejected +} + +/// +/// Message information +/// +public class Message +{ + /// + /// Message ID + /// + public int id; + /// + /// Client ID + /// + public int clientId; + /// + /// Message contents + /// + public string contents; + /// + /// Does this message need to be censored? + /// + public bool needsToBeCensored; + /// + /// Message status/stage + /// + public MessageStatus status = MessageStatus.WaitingForModerator; + + /// + /// When was this message sent + /// + public DateTime sentAt; +} + +public class ChatRoomState +{ + /// + /// Access lock. + /// + public readonly object accessLock = new object(); + + /// + /// Last unique ID value generated. + /// + public int lastUniqueId; + + /// + /// List of all registered clients + /// + public List clients = new List(); + + /// + /// List of messages + /// + public List messages = new List(); +} diff --git a/Lab2-rest/ChatRoom/Controllers/ChatRoomController.cs b/Lab2-rest/ChatRoom/Controllers/ChatRoomController.cs new file mode 100644 index 0000000..bc1d1ca --- /dev/null +++ b/Lab2-rest/ChatRoom/Controllers/ChatRoomController.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ChatRoom; + +/// +/// Service. Class must be marked public, otherwise ASP.NET core runtime will not find it. +/// +/// Look into FromXXX attributes if you need to map inputs to custom parts of HTTP request. +/// +[Route("chatRoom")] +[ApiController] +public class ChatRoomController : ControllerBase +{ + /// + /// Service logic. This is created in Server.StartServer() and received through DI in constructor. + /// + private readonly ChatRoomLogic logic; + + /// + /// Constructor + /// + /// Logic to use. This will get passed through DI. + public ChatRoomController(ChatRoomLogic logic) + { + this.logic = logic; + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + [HttpPost("/registerClient")] + public ActionResult RegisterClient(string name) + { + return logic.RegisterClient(name); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + [HttpGet("/getStrikes")] + public ActionResult GetStrikes(int clientId) + { + return logic.GetStrikes(clientId); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + [HttpGet("/getBlockedUntil")] + public ActionResult GetBlockedUntil(int clientId) + { + return logic.GetBlockedUntil(clientId); + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending successful, can fail if user is blocked + [HttpPost("/sendMessage")] + public ActionResult SendMessage(int clientId, string contents, bool needsToBeCensored) + { + return logic.SendMessage(clientId, contents, needsToBeCensored); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + [HttpGet("/getNewMessage")] + public ActionResult GetNewMessage() + { + return logic.GetNewMessage(); + } + + /// + /// Reject a message + /// + /// Message ID + [HttpPost("/rejectMessage")] + public void RejectMessage(int messageId) + { + logic.RejectMessage(messageId); + } + + /// + /// Approve a message + /// + /// Message ID + [HttpPost("/approveMessage")] + public void ApproveMessage(int messageId) + { + logic.ApproveMessage(messageId); + } +} diff --git a/Lab2-rest/ChatRoom/Properties/launchSettings.json b/Lab2-rest/ChatRoom/Properties/launchSettings.json new file mode 100644 index 0000000..c20165d --- /dev/null +++ b/Lab2-rest/ChatRoom/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64988", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5125", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Lab2-rest/ChatRoom/Server.cs b/Lab2-rest/ChatRoom/Server.cs new file mode 100644 index 0000000..ebed60c --- /dev/null +++ b/Lab2-rest/ChatRoom/Server.cs @@ -0,0 +1,126 @@ + +using NLog; +using System.Net; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace ChatRoom; + +public class Server +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Program entry point. + /// + /// Command line arguments. + public static void Main(string[] args) + { + var self = new Server(); + self.Run(args); + } + + /// + /// Program body. + /// + /// Command line arguments. + private void Run(string[] args) + { + //configure logging + ConfigureLogging(); + + //indicate server is about to start + log.Info("Server is about to start"); + + //start the server + StartServer(args); + } + + /// + /// Configure loggin subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + /// + /// Starts integrated server. + /// + /// Command line arguments. + public 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); + }); + + //add and configure swagger documentation generator (http://127.0.0.1:5000/swagger/) + builder.Services.AddSwaggerGen(opts => { + //include code comments in swagger documentation + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + opts.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); + + //turn on support for web api controllers + builder.Services + .AddControllers() + .AddJsonOptions(opts => { + //this makes enumeration values to be strings instead of integers in opeanapi doc + opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + //add CORS policies + builder.Services.AddCors(cr => { + //allow everything from everywhere + cr.AddPolicy("allowAll", cp => { + cp.AllowAnyOrigin(); + cp.AllowAnyMethod(); + cp.AllowAnyHeader(); + }); + }); + + //publish the background logic as (an internal) service through dependency injection, + //otherwise it will not start until the first client calls into controller + builder.Services.AddSingleton(new ChatRoomLogic()); + + //build the server + var app = builder.Build(); + + //turn CORS policy on + app.UseCors("allowAll"); + + //turn on support for swagger doc web page + app.UseSwagger(); + app.UseSwaggerUI(); + + //turn on request routing + app.UseRouting(); + + //configure routes + app.MapControllerRoute( + name: "default", + pattern: "{controller}/{action=Index}/{id?}" + ); + + //run the server + app.Run(); + // app.RunAsync(); //use this if you need to implement background processing in the main thread + } +} diff --git a/Lab2-rest/ChatRoom/appsettings.json b/Lab2-rest/ChatRoom/appsettings.json new file mode 100644 index 0000000..ae459eb --- /dev/null +++ b/Lab2-rest/ChatRoom/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Lab2-rest/ChatRoomContract/ChatRoomClient.cs b/Lab2-rest/ChatRoomContract/ChatRoomClient.cs new file mode 100644 index 0000000..6f3f927 --- /dev/null +++ b/Lab2-rest/ChatRoomContract/ChatRoomClient.cs @@ -0,0 +1,961 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#nullable enable + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace ChatRoomContract +{ + using System = global::System; + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ChatRoomClient : IChatRoomService + { +#pragma warning disable 8618 + private string _baseUrl; +#pragma warning restore 8618 + + private System.Net.Http.HttpClient _httpClient; + private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + private Newtonsoft.Json.JsonSerializerSettings _instanceSettings; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ChatRoomClient(string baseUrl, System.Net.Http.HttpClient httpClient) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + BaseUrl = baseUrl; + _httpClient = httpClient; + Initialize(); + } + + private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() + { + var settings = new Newtonsoft.Json.JsonSerializerSettings(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + public string BaseUrl + { + get { return _baseUrl; } + set + { + _baseUrl = value; + if (!string.IsNullOrEmpty(_baseUrl) && !_baseUrl.EndsWith("/")) + _baseUrl += '/'; + } + } + + protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _instanceSettings ?? _settings.Value; } } + + static partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings); + + partial void Initialize(); + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task RegisterClientAsync(string? name) + { + return RegisterClientAsync(name, System.Threading.CancellationToken.None); + } + + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual int RegisterClient(string? name) + { + return System.Threading.Tasks.Task.Run(async () => await RegisterClientAsync(name, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task RegisterClientAsync(string? name, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "text/plain"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "registerClient" + urlBuilder_.Append("registerClient"); + urlBuilder_.Append('?'); + if (name != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("name")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(name, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetStrikesAsync(int? clientId) + { + return GetStrikesAsync(clientId, System.Threading.CancellationToken.None); + } + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual int GetStrikes(int clientId) + { + return System.Threading.Tasks.Task.Run(async () => await GetStrikesAsync(clientId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetStrikesAsync(int? clientId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getStrikes" + urlBuilder_.Append("getStrikes"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetBlockedUntilAsync(int? clientId) + { + return GetBlockedUntilAsync(clientId, System.Threading.CancellationToken.None); + } + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual System.DateTime? GetBlockedUntil(int clientId) + { + return System.Threading.Tasks.Task.Run(async () => await GetBlockedUntilAsync(clientId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetBlockedUntilAsync(int? clientId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getBlockedUntil" + urlBuilder_.Append("getBlockedUntil"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object.DateTime; + } + else if (status_ == 204) + { + return null; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task SendMessageAsync(int? clientId, string? contents, bool? needsToBeCensored) + { + return SendMessageAsync(clientId, contents, needsToBeCensored, System.Threading.CancellationToken.None); + } + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual bool SendMessage(int clientId, string contents, bool needsToBeCensored) + { + return System.Threading.Tasks.Task.Run(async () => await SendMessageAsync(clientId, contents, needsToBeCensored, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task SendMessageAsync(int? clientId, string? contents, bool? needsToBeCensored, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "text/plain"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "sendMessage" + urlBuilder_.Append("sendMessage"); + urlBuilder_.Append('?'); + if (clientId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("clientId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(clientId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (contents != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("contents")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(contents, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + if (needsToBeCensored != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("needsToBeCensored")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(needsToBeCensored, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetNewMessageAsync() + { + return GetNewMessageAsync(System.Threading.CancellationToken.None); + } + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual Message? GetNewMessage() + { + return System.Threading.Tasks.Task.Run(async () => await GetNewMessageAsync(System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetNewMessageAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "getNewMessage" + urlBuilder_.Append("getNewMessage"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else if (status_ == 204) + { + return null; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task RejectMessageAsync(int? messageId) + { + return RejectMessageAsync(messageId, System.Threading.CancellationToken.None); + } + + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual void RejectMessage(int messageId) + { + System.Threading.Tasks.Task.Run(async () => await RejectMessageAsync(messageId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Reject a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task RejectMessageAsync(int? messageId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "rejectMessage" + urlBuilder_.Append("rejectMessage"); + urlBuilder_.Append('?'); + if (messageId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("messageId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(messageId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + return; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual System.Threading.Tasks.Task ApproveMessageAsync(int? messageId) + { + return ApproveMessageAsync(messageId, System.Threading.CancellationToken.None); + } + + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual void ApproveMessage(int messageId) + { + System.Threading.Tasks.Task.Run(async () => await ApproveMessageAsync(messageId, System.Threading.CancellationToken.None)).GetAwaiter().GetResult(); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Approve a message + /// + /// Message ID + /// Success + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task ApproveMessageAsync(int? messageId, System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "approveMessage" + urlBuilder_.Append("approveMessage"); + urlBuilder_.Append('?'); + if (messageId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("messageId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(messageId, System.Globalization.CultureInfo.InvariantCulture))).Append('&'); + } + urlBuilder_.Length--; + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + return; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + protected struct ObjectResponseResult + { + public ObjectResponseResult(T responseObject, string responseText) + { + this.Object = responseObject; + this.Text = responseText; + } + + public T Object { get; } + + public string Text { get; } + } + + public bool ReadResponseAsString { get; set; } + + protected virtual async System.Threading.Tasks.Task> ReadObjectResponseAsync(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Threading.CancellationToken cancellationToken) + { + if (response == null || response.Content == null) + { + return new ObjectResponseResult(default(T)!, string.Empty); + } + + if (ReadResponseAsString) + { + var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + try + { + var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText, JsonSerializerSettings); + return new ObjectResponseResult(typedBody!, responseText); + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception); + } + } + else + { + try + { + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var streamReader = new System.IO.StreamReader(responseStream)) + using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader)) + { + var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings); + var typedBody = serializer.Deserialize(jsonTextReader); + return new ObjectResponseResult(typedBody!, string.Empty); + } + } + catch (Newtonsoft.Json.JsonException exception) + { + var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; + throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception); + } + } + } + + private string ConvertToString(object? value, System.Globalization.CultureInfo cultureInfo) + { + if (value == null) + { + return ""; + } + + if (value is System.Enum) + { + var name = System.Enum.GetName(value.GetType(), value); + if (name != null) + { + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) + as System.Runtime.Serialization.EnumMemberAttribute; + if (attribute != null) + { + return attribute.Value != null ? attribute.Value : name; + } + } + + var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted == null ? string.Empty : converted; + } + } + else if (value is bool) + { + return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[]) + { + return System.Convert.ToBase64String((byte[])value); + } + else if (value is string[]) + { + return string.Join(",", (string[])value); + } + else if (value.GetType().IsArray) + { + var valueArray = (System.Array)value; + var valueTextArray = new string[valueArray.Length]; + for (var i = 0; i < valueArray.Length; i++) + { + valueTextArray[i] = ConvertToString(valueArray.GetValue(i), cultureInfo); + } + return string.Join(",", valueTextArray); + } + + var result = System.Convert.ToString(value, cultureInfo); + return result == null ? "" : result; + } + } + + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : System.Exception + { + public int StatusCode { get; private set; } + + public string? Response { get; private set; } + + public System.Collections.Generic.IReadOnlyDictionary> Headers { get; private set; } + + public ApiException(string message, int statusCode, string? response, System.Collections.Generic.IReadOnlyDictionary> headers, System.Exception? innerException) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiException : ApiException + { + public TResult Result { get; private set; } + + public ApiException(string message, int statusCode, string? response, System.Collections.Generic.IReadOnlyDictionary> headers, TResult result, System.Exception? innerException) + : base(message, statusCode, response, headers, innerException) + { + Result = result; + } + } + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj b/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj new file mode 100644 index 0000000..ac5be2b --- /dev/null +++ b/Lab2-rest/ChatRoomContract/ChatRoomContract.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Lab2-rest/ChatRoomContract/IChatRoomService.cs b/Lab2-rest/ChatRoomContract/IChatRoomService.cs new file mode 100644 index 0000000..c7405cd --- /dev/null +++ b/Lab2-rest/ChatRoomContract/IChatRoomService.cs @@ -0,0 +1,74 @@ +namespace ChatRoomContract; + +/// +/// Minimal message description +/// +public class Message +{ + /// + /// Message ID + /// + public int id { get; set; } + /// + /// Message contents + /// + public string contents { get; set; } + /// + /// Does this message need to be censored? + /// + public bool needsToBeCensored { get; set; } +} + +/// +/// Chat room service contract +/// +public interface IChatRoomService +{ + /// + /// Register client with a name + /// + /// Name of client, can be duplicate between clients + /// Client ID + int RegisterClient(string name); + + /// + /// Get number of strikes a participant has + /// + /// Client ID + /// Number of strikes + int GetStrikes(int clientId); + + /// + /// Get timestamp until when the client is blocked + /// + /// Client ID + /// Optional datetime object + DateTime? GetBlockedUntil(int clientId); + + /// + /// Send a message, will be given to a moderator to be approved + /// + /// Client ID + /// Message contents + /// Does this message need to be censored? + /// Was sending successful, can fail if user is blocked + bool SendMessage(int clientId, string contents, bool needsToBeCensored); + + /// + /// Get the next message which hasn't been approved or rejected + /// + /// Message object. Returns null if there is no message + Message? GetNewMessage(); + + /// + /// Reject a message + /// + /// Message ID + void RejectMessage(int messageId); + + /// + /// Approve a message + /// + /// Message ID + void ApproveMessage(int messageId); +} diff --git a/Lab2-rest/Lab2-rest.sln b/Lab2-rest/Lab2-rest.sln new file mode 100644 index 0000000..1012147 --- /dev/null +++ b/Lab2-rest/Lab2-rest.sln @@ -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", "{5F24CB9F-866F-4104-A6FB-A60371E6A85F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{CC5134F2-1127-4AF2-8CAF-1EFEAF31F5C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moderator", "Moderator\Moderator.csproj", "{572058F8-3C35-463C-95E9-A056A872FE36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Participant", "Participant\Participant.csproj", "{E2193712-FABC-4447-A291-AE0C29E80DD8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F24CB9F-866F-4104-A6FB-A60371E6A85F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F24CB9F-866F-4104-A6FB-A60371E6A85F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F24CB9F-866F-4104-A6FB-A60371E6A85F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F24CB9F-866F-4104-A6FB-A60371E6A85F}.Release|Any CPU.Build.0 = Release|Any CPU + {CC5134F2-1127-4AF2-8CAF-1EFEAF31F5C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC5134F2-1127-4AF2-8CAF-1EFEAF31F5C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC5134F2-1127-4AF2-8CAF-1EFEAF31F5C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC5134F2-1127-4AF2-8CAF-1EFEAF31F5C0}.Release|Any CPU.Build.0 = Release|Any CPU + {572058F8-3C35-463C-95E9-A056A872FE36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {572058F8-3C35-463C-95E9-A056A872FE36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {572058F8-3C35-463C-95E9-A056A872FE36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {572058F8-3C35-463C-95E9-A056A872FE36}.Release|Any CPU.Build.0 = Release|Any CPU + {E2193712-FABC-4447-A291-AE0C29E80DD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2193712-FABC-4447-A291-AE0C29E80DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2193712-FABC-4447-A291-AE0C29E80DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2193712-FABC-4447-A291-AE0C29E80DD8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {39207399-CA34-4BBB-BE63-E7BBCD0B52C5} + EndGlobalSection +EndGlobal diff --git a/Lab2-rest/Moderator/Moderator.cs b/Lab2-rest/Moderator/Moderator.cs new file mode 100644 index 0000000..d9b0c4c --- /dev/null +++ b/Lab2-rest/Moderator/Moderator.cs @@ -0,0 +1,92 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Moderator; + +internal class Moderator +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + Console.Title = $"Moderator | {name} | {clientId}"; + + while (true) + { + var message = chatRoom.GetNewMessage(); + if (message != null) + { + log.Info($"Checking message ({message.id}): {message.contents}"); + Thread.Sleep(500); + + if (message.needsToBeCensored) + { + chatRoom.RejectMessage(message.id); + } + else + { + chatRoom.ApproveMessage(message.id); + } + } + + Thread.Sleep(1 * 1000); + } + } + + private void Run() + { + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomClient("http://127.0.0.1:5000", new HttpClient()); + + try + { + RunConnection(client); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + static void Main(string[] args) + { + var self = new Moderator(); + self.Run(); + } +} diff --git a/Lab2-rest/Moderator/Moderator.csproj b/Lab2-rest/Moderator/Moderator.csproj new file mode 100644 index 0000000..a10c28d --- /dev/null +++ b/Lab2-rest/Moderator/Moderator.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Lab2-rest/Participant/Participant.cs b/Lab2-rest/Participant/Participant.cs new file mode 100644 index 0000000..870d5d5 --- /dev/null +++ b/Lab2-rest/Participant/Participant.cs @@ -0,0 +1,86 @@ +using ChatRoomContract; +using NLog; +using Bogus; + +namespace Participant; + +internal class Participant +{ + /// + /// Logger for this class. + /// + Logger log = LogManager.GetCurrentClassLogger(); + + /// + /// Configures logging subsystem. + /// + private void ConfigureLogging() + { + var config = new NLog.Config.LoggingConfiguration(); + + var console = + new NLog.Targets.ConsoleTarget("console") + { + Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}" + }; + config.AddTarget(console); + config.AddRuleForAllLevels(console); + + LogManager.Configuration = config; + } + + private void RunConnection(IChatRoomService chatRoom) + { + var faker = new Faker("en"); + var rnd = new Random(); + + var name = faker.Name.FullName(); + int clientId = chatRoom.RegisterClient(name); + log.Info($"Registered with client id {clientId}"); + + while (true) + { + int strikes = chatRoom.GetStrikes(clientId); + Console.Title = $"Participant | {name} | {clientId} | {strikes} Strikes"; + + var message = string.Join(" ", faker.Lorem.Words(5)); + bool needsToBeCensored = rnd.Next(0, 100) > 50; + if (chatRoom.SendMessage(clientId, message, needsToBeCensored)) { + log.Info("Sent message"); + } else { + log.Info("Failed to send message, blocked"); + } + + Thread.Sleep(2 * 1000); + } + } + + private void Run() + { + ConfigureLogging(); + + while (true) + { + var client = new ChatRoomClient("http://127.0.0.1:5000", new HttpClient()); + + try + { + RunConnection(client); + } + catch (Exception e) + { + //log whatever exception to console + log.Warn(e, "Unhandled exception caught. Will restart main loop."); + + //prevent console spamming + Thread.Sleep(2000); + } + } + } + + static void Main(string[] args) + { + var self = new Participant(); + self.Run(); + } +} diff --git a/Lab2-rest/Participant/Participant.csproj b/Lab2-rest/Participant/Participant.csproj new file mode 100644 index 0000000..a10c28d --- /dev/null +++ b/Lab2-rest/Participant/Participant.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + +