diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4ba1ecd
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*]
+end_of_line = crlf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.csproj]
+indent_size = 2
+
+[*.json]
+indent_size
+= 2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c7b568a..a915aee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,11 @@
.vs/*
.idea/*
+Logs/*
*/obj/*
*/bin/*
*.user
*.dat
*.mp3
*.ttf
-*.json
*.env
*.dll
-Boderator/*
-BoderatorTest/*
-BoderatorWeb/*
-BoderatorWebTest/*
diff --git a/ArmaForces.Boderator.BotService.Tests/ArmaForces.Boderator.BotService.Tests.csproj b/ArmaForces.Boderator.BotService.Tests/ArmaForces.Boderator.BotService.Tests.csproj
new file mode 100644
index 0000000..79f7a73
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/ArmaForces.Boderator.BotService.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net6.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/ArmaForces.Boderator.BotService.Tests/Features/Health/HealthControllerTests.cs b/ArmaForces.Boderator.BotService.Tests/Features/Health/HealthControllerTests.cs
new file mode 100644
index 0000000..350f069
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/Features/Health/HealthControllerTests.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using ArmaForces.Boderator.BotService.Tests.TestUtilities.TestBases;
+using ArmaForces.Boderator.BotService.Tests.TestUtilities.TestFixtures;
+using ArmaForces.Boderator.Core.Tests.TestUtilities;
+using Xunit;
+
+namespace ArmaForces.Boderator.BotService.Tests.Features.Health
+{
+ [Trait("Category", "Integration")]
+ public class HealthControllerTests : ApiTestBase
+ {
+ public HealthControllerTests(TestApiServiceFixture testApi)
+ : base(testApi) { }
+
+ [Fact]
+ public async Task Ping_AllOk_ReturnsPong()
+ {
+ var result = await HttpGetAsync("api/health/ping");
+
+ result.ShouldBeSuccess("pong");
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/CollectionsNames.cs b/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/CollectionsNames.cs
new file mode 100644
index 0000000..33c1494
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/CollectionsNames.cs
@@ -0,0 +1,7 @@
+namespace ArmaForces.Boderator.BotService.Tests.TestUtilities.Collections
+{
+ internal static class CollectionsNames
+ {
+ public const string ApiTest = "TestAPI";
+ }
+}
diff --git a/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/Definitions/TestApiCollection.cs b/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/Definitions/TestApiCollection.cs
new file mode 100644
index 0000000..d65920c
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/TestUtilities/Collections/Definitions/TestApiCollection.cs
@@ -0,0 +1,13 @@
+using ArmaForces.Boderator.BotService.Tests.TestUtilities.TestFixtures;
+using Xunit;
+
+namespace ArmaForces.Boderator.BotService.Tests.TestUtilities.Collections.Definitions
+{
+ [CollectionDefinition(CollectionsNames.ApiTest)]
+ public class TestApiCollection : ICollectionFixture
+ {
+ // This class has no code, and is never created. Its purpose is simply
+ // to be the place to apply [CollectionDefinition] and all the
+ // ICollectionFixture<> interfaces.
+ }
+}
diff --git a/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestBases/ApiTestBase.cs b/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestBases/ApiTestBase.cs
new file mode 100644
index 0000000..2ff23ea
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestBases/ApiTestBase.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using ArmaForces.Boderator.BotService.Tests.TestUtilities.Collections;
+using ArmaForces.Boderator.BotService.Tests.TestUtilities.TestFixtures;
+using CSharpFunctionalExtensions;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace ArmaForces.Boderator.BotService.Tests.TestUtilities.TestBases
+{
+ ///
+ /// Base class for integration tests involving API.
+ /// Provider test server and methods to invoke endpoints.
+ ///
+ [Collection(CollectionsNames.ApiTest)]
+ public abstract class ApiTestBase
+ {
+ private readonly HttpClient _httpClient;
+
+ protected IServiceProvider Provider { get; }
+
+ protected ApiTestBase(TestApiServiceFixture testApi)
+ {
+ _httpClient = testApi.HttpClient;
+
+ Provider = new ServiceCollection()
+ .BuildServiceProvider();
+ }
+
+ protected async Task> HttpGetAsync(string path)
+ {
+ return await HttpGetAsync(path)
+ .Bind(DeserializeContent);
+ }
+
+ protected async Task> HttpGetAsync(string path)
+ {
+ var httpResponseMessage = await _httpClient.GetAsync(path);
+ if (httpResponseMessage.IsSuccessStatusCode)
+ {
+ return await httpResponseMessage.Content.ReadAsStringAsync();
+ }
+
+ var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
+ var error = string.IsNullOrWhiteSpace(responseBody)
+ ? httpResponseMessage.ReasonPhrase
+ : responseBody;
+
+ return Result.Failure(error);
+ }
+
+ protected async Task HttpPostAsync(string path, T body)
+ {
+ var httpResponseMessage = await _httpClient.PostAsync(path, new StringContent(JsonConvert.SerializeObject(body)));
+ if (httpResponseMessage.IsSuccessStatusCode)
+ {
+ return Result.Success();
+ }
+
+ var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
+ var error = string.IsNullOrWhiteSpace(responseBody)
+ ? httpResponseMessage.ReasonPhrase
+ : responseBody;
+
+ return Result.Failure(error);
+ }
+
+ private static Result DeserializeContent(string content)
+ => JsonConvert.DeserializeObject(content);
+ }
+}
diff --git a/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestFixtures/TestApiServiceFixture.cs b/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestFixtures/TestApiServiceFixture.cs
new file mode 100644
index 0000000..2cfcf8f
--- /dev/null
+++ b/ArmaForces.Boderator.BotService.Tests/TestUtilities/TestFixtures/TestApiServiceFixture.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Net.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace ArmaForces.Boderator.BotService.Tests.TestUtilities.TestFixtures
+{
+ public class TestApiServiceFixture : IDisposable
+ {
+ private const int Port = 43421;
+ private readonly SocketsHttpHandler _socketsHttpHandler;
+ private readonly IHost _host;
+
+ public HttpClient HttpClient { get; }
+
+ public TestApiServiceFixture()
+ {
+ _socketsHttpHandler = new SocketsHttpHandler();
+ HttpClient = new HttpClient(_socketsHttpHandler)
+ {
+ BaseAddress = new Uri($"http://localhost:{Port}")
+ };
+
+ _host = Host.CreateDefaultBuilder()
+ .ConfigureWebHostDefaults(ConfigureWebBuilder())
+ .Build();
+
+ _host.Start();
+ }
+
+ public void Dispose()
+ {
+ HttpClient.Dispose();
+ _socketsHttpHandler.Dispose();
+ _host.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private static Action ConfigureWebBuilder() => webBuilder =>
+ {
+ webBuilder.UseStartup();
+ webBuilder.UseKestrel(x => x.ListenLocalhost(Port));
+ };
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/ArmaForces.Boderator.BotService.csproj b/ArmaForces.Boderator.BotService/ArmaForces.Boderator.BotService.csproj
new file mode 100644
index 0000000..6a4c23b
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/ArmaForces.Boderator.BotService.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net6.0
+ a9e33227-0f21-4863-9830-8aa69ac1e928
+ Linux
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Tests
+
+
+
+
diff --git a/ArmaForces.Boderator.BotService/Configuration/BoderatorConfiguration.cs b/ArmaForces.Boderator.BotService/Configuration/BoderatorConfiguration.cs
new file mode 100644
index 0000000..31cefa7
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Configuration/BoderatorConfiguration.cs
@@ -0,0 +1,7 @@
+namespace ArmaForces.Boderator.BotService.Configuration
+{
+ internal record BoderatorConfiguration
+ {
+ public string DiscordToken { get; init; } = string.Empty;
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Configuration/BoderatorConfigurationFactory.cs b/ArmaForces.Boderator.BotService/Configuration/BoderatorConfigurationFactory.cs
new file mode 100644
index 0000000..302c26f
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Configuration/BoderatorConfigurationFactory.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections;
+using System.Configuration;
+
+namespace ArmaForces.Boderator.BotService.Configuration
+{
+ internal class BoderatorConfigurationFactory
+ {
+ private readonly IDictionary _environmentVariables;
+
+ public BoderatorConfigurationFactory()
+ {
+ _environmentVariables = Environment.GetEnvironmentVariables();
+ }
+
+ // TODO: Consider making this a bit more automatic so configuration is easily extensible
+ public BoderatorConfiguration CreateConfiguration() => new BoderatorConfiguration
+ {
+ DiscordToken = GetStringValue(nameof(BoderatorConfiguration.DiscordToken))
+ };
+
+ private string GetStringValue(string variableName)
+ {
+ var fullVariableName = $"AF_Boderator_{variableName}";
+ var value = _environmentVariables[fullVariableName];
+
+ return value is not null
+ ? (string) value
+ : throw new ConfigurationErrorsException($"Variable {fullVariableName} does not exist.");
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Dockerfile b/ArmaForces.Boderator.BotService/Dockerfile
new file mode 100644
index 0000000..cdf656b
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Dockerfile
@@ -0,0 +1,22 @@
+#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
+WORKDIR /app
+EXPOSE 80
+EXPOSE 443
+
+FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+WORKDIR /src
+COPY ["ArmaForces.Boderator.WebService/ArmaForces.Boderator.WebService.csproj", "ArmaForces.Boderator.WebService/"]
+RUN dotnet restore "ArmaForces.Boderator.WebService/ArmaForces.Boderator.WebService.csproj"
+COPY . .
+WORKDIR "/src/ArmaForces.Boderator.WebService"
+RUN dotnet build "ArmaForces.Boderator.WebService.csproj" -c Release -o /app/build
+
+FROM build AS publish
+RUN dotnet publish "ArmaForces.Boderator.WebService.csproj" -c Release -o /app/publish
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "ArmaForces.Boderator.WebService.dll"]
\ No newline at end of file
diff --git a/ArmaForces.Boderator.BotService/Documentation/DocumentationExtensions.cs b/ArmaForces.Boderator.BotService/Documentation/DocumentationExtensions.cs
new file mode 100644
index 0000000..1f1c59b
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Documentation/DocumentationExtensions.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OpenApi.Models;
+
+namespace ArmaForces.Boderator.BotService.Documentation
+{
+ internal static class DocumentationExtensions
+ {
+ private const string DefaultSwaggerJsonUrl = "/swagger/v3/swagger.json";
+
+ public static IServiceCollection AddDocumentation(this IServiceCollection services, OpenApiInfo openApiConfig)
+ {
+ return services.AddSwaggerGen(
+ options =>
+ {
+ options.SwaggerDoc(openApiConfig.Version, openApiConfig);
+ });
+ }
+
+ public static IApplicationBuilder AddDocumentation(
+ this IApplicationBuilder app,
+ OpenApiInfo openApiConfig,
+ string url = DefaultSwaggerJsonUrl)
+ {
+ return app.UseSwagger()
+ .UseSwaggerUI(
+ options => options.SwaggerEndpoint(
+ url,
+ openApiConfig.Title))
+ .UseReDoc(
+ options =>
+ {
+ options.DocumentTitle = openApiConfig.Title;
+ options.SpecUrl = url;
+ });
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Features/DiscordClient/DTOs/DiscordServiceStatusDto.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/DTOs/DiscordServiceStatusDto.cs
new file mode 100644
index 0000000..def56b2
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/DTOs/DiscordServiceStatusDto.cs
@@ -0,0 +1,11 @@
+using Discord;
+
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient.DTOs
+{
+ public class DiscordServiceStatusDto
+ {
+ public ConnectionState ConnectionState { get; init; }
+ public LoginState LoginState { get; init; }
+ public UserStatus ClientState { get; init; }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Features/DiscordClient/DiscordService.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/DiscordService.cs
new file mode 100644
index 0000000..1459051
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/DiscordService.cs
@@ -0,0 +1,64 @@
+using System.Threading;
+using System.Threading.Tasks;
+using ArmaForces.Boderator.BotService.Configuration;
+using ArmaForces.Boderator.BotService.Features.DiscordClient.DTOs;
+using Discord;
+using Discord.WebSocket;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient
+{
+ internal sealed class DiscordService : IDiscordService, IHostedService
+ {
+ private readonly DiscordSocketClient _discordClient;
+ private readonly ILogger _logger;
+
+ private readonly string _token;
+
+ public DiscordService(
+ DiscordSocketClient discordClient,
+ BoderatorConfiguration configuration,
+ ILogger logger)
+ {
+ _logger = logger;
+ _discordClient = discordClient;
+ _token = configuration.DiscordToken;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Discord Service started");
+ await _discordClient.LoginAsync(TokenType.Bot, _token);
+ await _discordClient.StartAsync();
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.Run(() =>
+ {
+ _discordClient.Dispose();
+ _logger.LogInformation("Discord Service stopped");
+ }, cancellationToken);
+
+ public DiscordServiceStatusDto GetDiscordClientStatus()
+ {
+ _logger.LogInformation(
+ "Current Discord Bot status: Login: {BotStatus} | " +
+ "Connection: {ConnectionStatus} | " +
+ "Status: {ClientStatus}",
+ _discordClient.LoginState,
+ _discordClient.ConnectionState,
+ _discordClient.Status);
+
+ return new DiscordServiceStatusDto
+ {
+ ConnectionState = _discordClient.ConnectionState,
+ LoginState = _discordClient.LoginState,
+ ClientState = _discordClient.Status
+ };
+ }
+
+ public async Task SetBotStatus(string newStatus, ActivityType statusType) =>
+ await _discordClient.SetGameAsync(newStatus, type: statusType);
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Features/DiscordClient/IDiscordService.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/IDiscordService.cs
new file mode 100644
index 0000000..5e969ca
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/IDiscordService.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+using ArmaForces.Boderator.BotService.Features.DiscordClient.DTOs;
+using Discord;
+
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient
+{
+ internal interface IDiscordService
+ {
+ DiscordServiceStatusDto GetDiscordClientStatus();
+
+ Task SetBotStatus(string newStatus, ActivityType statusType);
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordServiceCollectionExtensions.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3c1c233
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordServiceCollectionExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient.Infrastructure.DependencyInjection
+{
+ internal static class DiscordServiceCollectionExtensions
+ {
+ public static IServiceCollection AddDiscordClient(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(DiscordSocketClientFactory.CreateDiscordClient)
+ .AddSingleton()
+ .AddHostedService();
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordSocketClientFactory.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordSocketClientFactory.cs
new file mode 100644
index 0000000..a694db1
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/DependencyInjection/DiscordSocketClientFactory.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using ArmaForces.Boderator.BotService.Features.DiscordClient.Infrastructure.Logging;
+using Discord.WebSocket;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient.Infrastructure.DependencyInjection
+{
+ internal static class DiscordSocketClientFactory
+ {
+ private static bool _isClientCreated;
+
+ public static DiscordSocketClient CreateDiscordClient(IServiceProvider serviceProvider)
+ {
+ if (_isClientCreated)
+ throw new InvalidOperationException("Only one Discord client can be created!");
+
+ _isClientCreated = true;
+
+ var discordSocketConfig = new DiscordSocketConfig
+ {
+ AlwaysDownloadUsers = true,
+ MessageCacheSize = 100000
+ };
+ var client = new DiscordSocketClient(discordSocketConfig);
+
+ ConfigureLogging(client, serviceProvider);
+
+ return client;
+ }
+
+ private static void ConfigureLogging(DiscordSocketClient client, IServiceProvider serviceProvider)
+ {
+ var logger = serviceProvider.GetRequiredService>();
+ client.Log += message => Task.Run(() => message.LogWithLoggerAsync(logger));
+ client.Connected += () => Task.Run(() => logger.LogInformation("Discord connected"));
+ }
+ }
+}
diff --git a/ArmaforcesMissionBot/Extensions/LogMessageExtensions.cs b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/Logging/LogMessageExtensions.cs
similarity index 89%
rename from ArmaforcesMissionBot/Extensions/LogMessageExtensions.cs
rename to ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/Logging/LogMessageExtensions.cs
index 80d3d9f..dc33aa1 100644
--- a/ArmaforcesMissionBot/Extensions/LogMessageExtensions.cs
+++ b/ArmaForces.Boderator.BotService/Features/DiscordClient/Infrastructure/Logging/LogMessageExtensions.cs
@@ -1,9 +1,11 @@
-using System;
+using System;
using System.Threading.Tasks;
using Discord;
using Microsoft.Extensions.Logging;
-namespace ArmaforcesMissionBot.Extensions
+// Disabled warning as messages are coming from Discord logger
+// ReSharper disable TemplateIsNotCompileTimeConstantProblem
+namespace ArmaForces.Boderator.BotService.Features.DiscordClient.Infrastructure.Logging
{
internal static class LogMessageExtensions
{
@@ -13,7 +15,7 @@ public static Task LogWithLoggerAsync(this LogMessage logMessage, ILogger logger
return Task.CompletedTask;
}
- public static void LogWithLogger(this LogMessage logMessage, ILogger logger)
+ private static void LogWithLogger(this LogMessage logMessage, ILogger logger)
{
if (logMessage.Exception != null)
{
diff --git a/ArmaForces.Boderator.BotService/Features/Health/HealthController.cs b/ArmaForces.Boderator.BotService/Features/Health/HealthController.cs
new file mode 100644
index 0000000..4e0c1d2
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Features/Health/HealthController.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ArmaForces.Boderator.BotService.Features.Health
+{
+ ///
+ /// Allows obtaining application status.
+ ///
+ [Route("api/[controller]")]
+ public class HealthController : Controller
+ {
+ ///
+ /// Responds to a ping.
+ ///
+ [HttpGet("ping")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IActionResult Ping() => Ok("pong");
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Logging/SerilogConfigurationExtensions.cs b/ArmaForces.Boderator.BotService/Logging/SerilogConfigurationExtensions.cs
new file mode 100644
index 0000000..4bb2f8b
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Logging/SerilogConfigurationExtensions.cs
@@ -0,0 +1,15 @@
+using System;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+
+namespace ArmaForces.Boderator.BotService.Logging
+{
+ internal static class SerilogConfigurationExtensions
+ {
+ public static IHostBuilder AddSerilog(this IHostBuilder hostBuilder)
+ => hostBuilder.UseSerilog(ConfigureLogging());
+
+ private static Action ConfigureLogging() => (context, configuration) => configuration
+ .ReadFrom.Configuration(context.Configuration);
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Program.cs b/ArmaForces.Boderator.BotService/Program.cs
new file mode 100644
index 0000000..281be83
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Program.cs
@@ -0,0 +1,22 @@
+using ArmaForces.Boderator.BotService.Logging;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace ArmaForces.Boderator.BotService
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ private static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .AddSerilog()
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/Properties/launchSettings.json b/ArmaForces.Boderator.BotService/Properties/launchSettings.json
new file mode 100644
index 0000000..fc9df5e
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:60188/",
+ "sslPort": 44372
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "ArmaForces.Boderator.BotService": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api-docs/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "AF_Boderator_DiscordToken": ""
+ },
+ "applicationUrl": "https://localhost:5001;http://localhost:5000"
+ }
+ }
+}
\ No newline at end of file
diff --git a/ArmaForces.Boderator.BotService/Startup.cs b/ArmaForces.Boderator.BotService/Startup.cs
new file mode 100644
index 0000000..302dde0
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/Startup.cs
@@ -0,0 +1,68 @@
+using System;
+using ArmaForces.Boderator.BotService.Configuration;
+using ArmaForces.Boderator.BotService.Documentation;
+using ArmaForces.Boderator.BotService.Features.DiscordClient.Infrastructure.DependencyInjection;
+using ArmaForces.Boderator.Core.DependencyInjection;
+using Discord.WebSocket;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.OpenApi.Models;
+
+namespace ArmaForces.Boderator.BotService
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ private IConfiguration Configuration { get; }
+
+ private OpenApiInfo OpenApiConfiguration { get; } = new()
+ {
+ Title = "ArmaForces Boderator API",
+ Description = "API that does nothing. For now.",
+ Version = "v3",
+ Contact = new OpenApiContact
+ {
+ Name = "ArmaForces",
+ Url = new Uri("https://armaforces.com")
+ }
+ };
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddControllers();
+ services.AddDocumentation(OpenApiConfiguration);
+ services.AddSingleton(new BoderatorConfigurationFactory().CreateConfiguration());
+ services.AddDiscordClient();
+ services.AutoAddInterfacesAsScoped(typeof(Startup).Assembly);
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ app.AddDocumentation(OpenApiConfiguration);
+ }
+
+ app.UseHttpsRedirection();
+
+ app.UseRouting();
+
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/appsettings.Development.json b/ArmaForces.Boderator.BotService/appsettings.Development.json
new file mode 100644
index 0000000..d3c3868
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/appsettings.Development.json
@@ -0,0 +1,37 @@
+{
+ "Serilog": {
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{ThreadId}] {SourceContext} {Message:lj} {Properties}{NewLine}{Exception}"
+ }
+ },
+ {
+ "Name": "Debug",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{ThreadId}] {SourceContext} {Message:lj} {Properties}{NewLine}{Exception}"
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "../Logs/log.txt",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit": 7,
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{ThreadId}] {SourceContext} {Message:lj} {Properties}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithThreadId"
+ ]
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.BotService/appsettings.json b/ArmaForces.Boderator.BotService/appsettings.json
new file mode 100644
index 0000000..d76d42c
--- /dev/null
+++ b/ArmaForces.Boderator.BotService/appsettings.json
@@ -0,0 +1,43 @@
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "ArmaForces": "Verbose",
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore.Hosting.Diagnostics" : "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{ThreadId}] {SourceContext} {Message:lj} {Properties}{NewLine}{Exception}"
+ }
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "Logs/log.txt",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit": 7,
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{ThreadId}] {SourceContext} {Message:lj} {Properties}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithThreadId"
+ ]
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ArmaForces.Boderator.Core.Tests/ArmaForces.Boderator.Core.Tests.csproj b/ArmaForces.Boderator.Core.Tests/ArmaForces.Boderator.Core.Tests.csproj
new file mode 100644
index 0000000..34d60e5
--- /dev/null
+++ b/ArmaForces.Boderator.Core.Tests/ArmaForces.Boderator.Core.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net6.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/ArmaForces.Boderator.Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/ArmaForces.Boderator.Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..232902c
--- /dev/null
+++ b/ArmaForces.Boderator.Core.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,59 @@
+using System.Linq;
+using ArmaForces.Boderator.Core.DependencyInjection;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace ArmaForces.Boderator.Core.Tests.DependencyInjection
+{
+ public class ServiceCollectionExtensionsTests
+ {
+ [Fact]
+ [Trait("Category", "Unit")]
+ public void AutoAddInterfacesAsScoped_EmptyCollection_RegistersOnlyInterfacesWithOneImplementation()
+ {
+ var serviceProvider = new ServiceCollection()
+ .AutoAddInterfacesAsScoped(typeof(ServiceCollectionExtensionsTests).Assembly)
+ .BuildServiceProvider();
+
+ using (new AssertionScope())
+ {
+ serviceProvider.GetService().Should().BeNull();
+ serviceProvider.GetService().Should().NotBeNull();
+ serviceProvider.GetService().Should().BeNull();
+ }
+ }
+
+ [Fact]
+ [Trait("Category", "Unit")]
+ public void AutoAddInterfacesAsScoped_ImplementationRegisteredAsSingleton_DoesNotReplaceExistingRegistration()
+ {
+ var serviceCollection = new ServiceCollection()
+ .AddSingleton()
+ .AutoAddInterfacesAsScoped(typeof(ServiceCollectionExtensionsTests).Assembly);
+
+ AssertServiceRegisteredCorrectly(serviceCollection, ServiceLifetime.Singleton);
+ }
+
+ private static void AssertServiceRegisteredCorrectly(IServiceCollection serviceCollection, ServiceLifetime expectedLifetime)
+ {
+ var expectedServiceDescriptor = new ServiceDescriptor(typeof(TService), typeof(TExpectedImplementation), expectedLifetime);
+
+ serviceCollection.Should()
+ .ContainSingle(descriptor => descriptor.ServiceType == typeof(TService))
+ .Which.Should()
+ .BeEquivalentTo(expectedServiceDescriptor, $"the service {nameof(TService)} was registered as {expectedLifetime} and should not be replaced");
+ }
+
+ private interface ITest1 { }
+
+ private interface ITest2 { }
+
+ private interface ITest3 { }
+
+ private class Test1 : ITest1 { }
+
+ private class Test2 : ITest1, ITest2 { }
+ }
+}
diff --git a/ArmaForces.Boderator.Core.Tests/TestUtilities/ResultAssertionsExtensions.cs b/ArmaForces.Boderator.Core.Tests/TestUtilities/ResultAssertionsExtensions.cs
new file mode 100644
index 0000000..cee12c8
--- /dev/null
+++ b/ArmaForces.Boderator.Core.Tests/TestUtilities/ResultAssertionsExtensions.cs
@@ -0,0 +1,24 @@
+using CSharpFunctionalExtensions;
+using FluentAssertions;
+using FluentAssertions.Execution;
+
+namespace ArmaForces.Boderator.Core.Tests.TestUtilities
+{
+ public static class ResultAssertionsExtensions
+ {
+ public static void ShouldBeSuccess(this Result result, T expectedValue)
+ {
+ using var scope = new AssertionScope();
+
+ if (result.IsSuccess)
+ {
+ result.Value.Should().BeEquivalentTo(expectedValue);
+ }
+ else
+ {
+ result.IsSuccess.Should().BeTrue();
+ result.Error.Should().BeNull();
+ }
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.Core/ArmaForces.Boderator.Core.csproj b/ArmaForces.Boderator.Core/ArmaForces.Boderator.Core.csproj
new file mode 100644
index 0000000..5a71804
--- /dev/null
+++ b/ArmaForces.Boderator.Core/ArmaForces.Boderator.Core.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net6.0
+ enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Tests
+
+
+
+
diff --git a/ArmaForces.Boderator.Core/BoderatorContext.cs b/ArmaForces.Boderator.Core/BoderatorContext.cs
new file mode 100644
index 0000000..50fda77
--- /dev/null
+++ b/ArmaForces.Boderator.Core/BoderatorContext.cs
@@ -0,0 +1,12 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace ArmaForces.Boderator.Core
+{
+ public class BoderatorContext : DbContext
+ {
+ public BoderatorContext() : base()
+ {
+
+ }
+ }
+}
diff --git a/ArmaForces.Boderator.Core/DependencyInjection/ServiceCollectionExtensions.cs b/ArmaForces.Boderator.Core/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..b529814
--- /dev/null
+++ b/ArmaForces.Boderator.Core/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ArmaForces.Boderator.Core.DependencyInjection
+{
+ public static class ServiceCollectionExtensions
+ {
+ ///
+ /// Adds all interfaces from with single implementation type as scoped service.
+ /// Does not replace existing registrations.
+ ///
+ /// Service collection where services will be registered
+ /// Assembly which will be searched for interfaces and implementations.
+ /// Service collection for method chaining.
+ public static IServiceCollection AutoAddInterfacesAsScoped(this IServiceCollection services, Assembly assembly)
+ {
+ assembly.DefinedTypes
+ .Where(x => x.ImplementedInterfaces.Any())
+ .SelectMany(
+ implementingClass => implementingClass.ImplementedInterfaces,
+ (implementingClass, implementedInterface) => new {implementedInterface, implementingClass})
+ .GroupBy(x => x.implementedInterface)
+ .Where(x => x.Count() == 1)
+ .Select(x => x.Single())
+ .Where(x => services.IsNotServiceRegistered(x.implementedInterface))
+ .ToList()
+ .ForEach(x => services.AddScoped(x.implementedInterface, x.implementingClass));
+
+ return services;
+ }
+
+ private static bool IsNotServiceRegistered(this IServiceCollection services, Type serviceType)
+ => !IsServiceRegistered(services, serviceType);
+
+ private static bool IsServiceRegistered(this IServiceCollection services, Type serviceType)
+ => services.Any(descriptor => descriptor.ServiceType == serviceType);
+ }
+}
diff --git a/ArmaForces.Boderator.sln b/ArmaForces.Boderator.sln
new file mode 100644
index 0000000..2977144
--- /dev/null
+++ b/ArmaForces.Boderator.sln
@@ -0,0 +1,55 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31205.134
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArmaForces.Boderator.Core", "ArmaForces.Boderator.Core\ArmaForces.Boderator.Core.csproj", "{A2849677-3590-45E2-BD3C-2C21B2C59BC6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArmaForces.Boderator.BotService.Tests", "ArmaForces.Boderator.BotService.Tests\ArmaForces.Boderator.BotService.Tests.csproj", "{85762E30-4C0D-4735-B40F-C8C40F73F4C3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArmaForces.Boderator.BotService", "ArmaForces.Boderator.BotService\ArmaForces.Boderator.BotService.csproj", "{250BF5C2-0750-4BE0-BFEA-35EAA9AB24F1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArmaForces.Boderator.Core.Tests", "ArmaForces.Boderator.Core.Tests\ArmaForces.Boderator.Core.Tests.csproj", "{7DE1D733-EBE7-4800-A5E2-6F0425EC93E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AttendanceExtractor", "AttendanceExtractor\AttendanceExtractor.csproj", "{1209FD95-80F1-4FBF-8251-387A85A7E203}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArmaForces.R3ReplaysConverter", "ArmaForces.R3ReplaysConverter\ArmaForces.R3ReplaysConverter.csproj", "{35C5A991-9DF3-4676-95EE-1E5496ED7155}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A2849677-3590-45E2-BD3C-2C21B2C59BC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2849677-3590-45E2-BD3C-2C21B2C59BC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2849677-3590-45E2-BD3C-2C21B2C59BC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2849677-3590-45E2-BD3C-2C21B2C59BC6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {85762E30-4C0D-4735-B40F-C8C40F73F4C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {85762E30-4C0D-4735-B40F-C8C40F73F4C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {85762E30-4C0D-4735-B40F-C8C40F73F4C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {85762E30-4C0D-4735-B40F-C8C40F73F4C3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {250BF5C2-0750-4BE0-BFEA-35EAA9AB24F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {250BF5C2-0750-4BE0-BFEA-35EAA9AB24F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {250BF5C2-0750-4BE0-BFEA-35EAA9AB24F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {250BF5C2-0750-4BE0-BFEA-35EAA9AB24F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7DE1D733-EBE7-4800-A5E2-6F0425EC93E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7DE1D733-EBE7-4800-A5E2-6F0425EC93E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7DE1D733-EBE7-4800-A5E2-6F0425EC93E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7DE1D733-EBE7-4800-A5E2-6F0425EC93E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1209FD95-80F1-4FBF-8251-387A85A7E203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1209FD95-80F1-4FBF-8251-387A85A7E203}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1209FD95-80F1-4FBF-8251-387A85A7E203}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1209FD95-80F1-4FBF-8251-387A85A7E203}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35C5A991-9DF3-4676-95EE-1E5496ED7155}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35C5A991-9DF3-4676-95EE-1E5496ED7155}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35C5A991-9DF3-4676-95EE-1E5496ED7155}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35C5A991-9DF3-4676-95EE-1E5496ED7155}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AB0E82AA-6B41-410A-BEC8-56458A25309E}
+ EndGlobalSection
+EndGlobal
diff --git a/ArmaForces.Boderator.sln.DotSettings b/ArmaForces.Boderator.sln.DotSettings
new file mode 100644
index 0000000..f4b9f27
--- /dev/null
+++ b/ArmaForces.Boderator.sln.DotSettings
@@ -0,0 +1,5 @@
+
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/ArmaForces.R3ReplaysConverter/ArmaForces.R3ReplaysConverter.csproj b/ArmaForces.R3ReplaysConverter/ArmaForces.R3ReplaysConverter.csproj
new file mode 100644
index 0000000..656841b
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/ArmaForces.R3ReplaysConverter.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+
+
+
diff --git a/ArmaForces.R3ReplaysConverter/MissionAttendanceData.cs b/ArmaForces.R3ReplaysConverter/MissionAttendanceData.cs
new file mode 100644
index 0000000..60afc3f
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/MissionAttendanceData.cs
@@ -0,0 +1,12 @@
+namespace ArmaForces.R3ReplaysConverter;
+
+public record MissionAttendanceData
+{
+ public string ReplayFilePath { get; init; }
+ public string MapCode { get; init; }
+ public DateTimeOffset MissionDate { get; init; }
+ public string MissionName { get; init; }
+ public List Players { get; init; }
+
+ public long MissionId => MissionDate.ToUnixTimeSeconds();
+}
diff --git a/ArmaForces.R3ReplaysConverter/PlayerInfo.cs b/ArmaForces.R3ReplaysConverter/PlayerInfo.cs
new file mode 100644
index 0000000..cde19fa
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/PlayerInfo.cs
@@ -0,0 +1,10 @@
+namespace ArmaForces.R3ReplaysConverter;
+
+public record PlayerInfo
+{
+ public string Name { get; init; } = string.Empty;
+
+ public ulong SteamUid { get; init; }
+
+ public string[] OtherNames { get; init; } = Array.Empty();
+}
diff --git a/ArmaForces.R3ReplaysConverter/Program.cs b/ArmaForces.R3ReplaysConverter/Program.cs
new file mode 100644
index 0000000..6f042e7
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/Program.cs
@@ -0,0 +1,16 @@
+// See https://aka.ms/new-console-template for more information
+
+using ArmaForces.R3ReplaysConverter;
+
+await ReplaysProcessing.ProcessParallel();
+
+// using var memoryStream = new MemoryStream();
+
+// gzipStream.CopyTo(memoryStream);
+
+// var json = Encoding.UTF8.GetString(memoryStream.ToArray());
+
+// var ddd = JsonSerializer.Deserialize(memoryStream, typeof(JsonObject), JsonSerializerOptions.Default);
+
+Console.WriteLine("Hello, World!");
+return;
diff --git a/ArmaForces.R3ReplaysConverter/R3Entry.cs b/ArmaForces.R3ReplaysConverter/R3Entry.cs
new file mode 100644
index 0000000..34ce9c5
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/R3Entry.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace ArmaForces.R3ReplaysConverter;
+
+public record R3Entry
+{
+ public long Id { get; init; }
+
+ public string PlayerId { get; init; } = "0";
+
+ public string Type { get; init; } = string.Empty;
+
+ public string Value { get; init; } = string.Empty;
+
+ public long MissionTime { get; init; }
+};
\ No newline at end of file
diff --git a/ArmaForces.R3ReplaysConverter/R3EntryType.cs b/ArmaForces.R3ReplaysConverter/R3EntryType.cs
new file mode 100644
index 0000000..057c9e2
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/R3EntryType.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace ArmaForces.R3ReplaysConverter;
+
+public enum R3EntryType
+{
+ [JsonPropertyName("markers")]
+ markers,
+
+ [JsonPropertyName("positions_infantry")]
+ positions_infantry,
+
+ [JsonPropertyName("player_disconnected")]
+ player_disconnected,
+
+ [JsonPropertyName("unit_killed")]
+ unit_killed
+}
diff --git a/ArmaForces.R3ReplaysConverter/R3UnitEntry.cs b/ArmaForces.R3ReplaysConverter/R3UnitEntry.cs
new file mode 100644
index 0000000..94a7f7b
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/R3UnitEntry.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace ArmaForces.R3ReplaysConverter;
+
+public record R3UnitEntry
+{
+ ///
+ /// In format:
+ /// [SIDE] [GROUP]: [INDEX_IN_GROUP] ([PLAYER_NAME]) REMOTE
+ ///
+ /// B Alpha 1-1: 1 (nomus) REMOTE
+ public string Unit { get; init; } = string.Empty;
+
+ ///
+ /// It should be equal to Steam UID.
+ ///
+ public string Id { get; init; } = string.Empty;
+
+ public double[] Pos { get; init; } = {0, 0, 0};
+
+ public double Dir { get; init; }
+
+ public string Ico { get; init; } = string.Empty;
+
+ ///
+ /// ???
+ ///
+ public string Fac { get; init; } = string.Empty;
+
+ public string Grp { get; init; } = string.Empty;
+
+ public string Ldr { get; init; } = string.Empty;
+}
diff --git a/ArmaForces.R3ReplaysConverter/ReplaysProcessingParallel.cs b/ArmaForces.R3ReplaysConverter/ReplaysProcessingParallel.cs
new file mode 100644
index 0000000..489d6d6
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/ReplaysProcessingParallel.cs
@@ -0,0 +1,116 @@
+using System.Globalization;
+using System.IO.Compression;
+using System.Text;
+using System.Text.Json;
+
+namespace ArmaForces.R3ReplaysConverter;
+
+public static class ReplaysProcessing
+{
+ private const string ReplaysFolder = "D:\\ArmaForcesR3Replays";
+ private static readonly string DataPath = Path.Join(Directory.GetCurrentDirectory(), "attendance-data.json");
+ private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions();
+
+ private static readonly string[] MissionsWithBrokenUnitsJsons =
+ {
+ "208#Mydlana_Misja_29_09#sara#26#29-09-2018 20-33-30#",
+ "2791#"
+ };
+
+ private static readonly object DataLock = new object();
+
+ public static async Task ProcessParallel()
+ {
+ var data = await GetExistingData();
+ var replays = GetReplaysToProcess(data);
+
+ await Parallel.ForEachAsync(replays, (replay, token) => ProcessReplay(replay, data));
+
+ SaveData(data);
+ }
+
+ public static async Task ProcessSequential()
+ {
+ var data = await GetExistingData();
+ var replays = GetReplaysToProcess(data);
+
+ foreach (var replay in replays)
+ {
+ await ProcessReplay(replay, data);
+ }
+
+ SaveData(data);
+ }
+
+ private static IEnumerable GetReplaysToProcess(List ddd)
+ {
+ var processedReplays = ddd.Select(x => x.ReplayFilePath).ToList();
+
+ Console.WriteLine($"Found {processedReplays.Count} already processed replays");
+ var remainingReplays = Directory.GetFiles(ReplaysFolder)
+ .Where(x => !processedReplays.Contains(x))
+ // .Where(replay => !MissionsWithBrokenUnitsJsons.Any(replay.Contains))
+ // 208 requires special handling for "" in group name
+ // .Where(x => !x.Contains("208#Mydlana_Misja_29_09#sara#26#29-09-2018 20-33-30#.json"))
+ .ToList();
+
+ Console.WriteLine($"{remainingReplays.Count} replays remaining");
+
+ return remainingReplays; //.First(x => x.Contains("77#"));
+ }
+
+ private static async Task> GetExistingData()
+ {
+ return File.Exists(DataPath)
+ ? JsonSerializer.Deserialize>(await File.ReadAllTextAsync(DataPath), SerializerOptions)
+ ?? new List()
+ : new List();
+ }
+
+ private static void SaveData(List data)
+ {
+ Console.WriteLine($"Saving data for {data.Count} replays.");
+ File.WriteAllText(DataPath, JsonSerializer.Serialize(data, SerializerOptions), Encoding.UTF8);
+ }
+
+ private static async ValueTask ProcessReplay(string replay, List data)
+ {
+ var replayFileName = Path.GetFileName(replay).Replace("#.json.gz", string.Empty);
+ var fileNameComponents = replayFileName.Split('#');
+ var replayIndex = fileNameComponents[0];
+ var missionName = fileNameComponents[1];
+ var mapCode = fileNameComponents[2];
+ var missionNumberThatDay = fileNameComponents[3];
+ var missionDateText = fileNameComponents[4];
+ var success = DateTimeOffset.TryParseExact(missionDateText, "dd-MM-yyyy HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var missionDate);
+ if (!success)
+ {
+
+ }
+
+ Console.WriteLine($"Processing {replayFileName}");
+
+ await using var fileStream = new FileStream(replay, FileMode.Open);
+ await using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
+ var players = await Utf8ReaderPartialRead.Run(gzipStream);
+
+ var attendanceData = new MissionAttendanceData
+ {
+ MapCode = mapCode,
+ MissionDate = missionDate,
+ MissionName = missionName,
+ Players = players,
+ ReplayFilePath = replay
+ };
+
+ lock (DataLock)
+ {
+ data.Add(attendanceData);
+
+ if (data.Count % 50 == 0)
+ {
+ SaveData(data);
+ }
+ }
+ }
+}
diff --git a/ArmaForces.R3ReplaysConverter/Utf8ReaderPartialRead.cs b/ArmaForces.R3ReplaysConverter/Utf8ReaderPartialRead.cs
new file mode 100644
index 0000000..3883273
--- /dev/null
+++ b/ArmaForces.R3ReplaysConverter/Utf8ReaderPartialRead.cs
@@ -0,0 +1,133 @@
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace ArmaForces.R3ReplaysConverter;
+
+public class Utf8ReaderPartialRead
+{
+ private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ private static List KnownUselessEventTypes = new List
+ {
+ "player_connected",
+ "player_disconnected",
+ "unit_killed",
+ "get_in",
+ "get_out",
+ "positions_vehicles"
+ };
+
+ public static List RunDebug(Stream stream)
+ {
+ var buffer = new byte[4096*2];
+ _ = stream.Read(buffer);
+ var debugReader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
+ Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
+ // GetMoreBytesFromStream(stream, ref buffer, ref debugReader);
+ if (debugReader.TokenType == JsonTokenType.StartObject)
+ {
+ JsonSerializer.Deserialize(debugReader.Read());
+ }
+
+ return new List();
+ }
+
+ public static async Task> Run(Stream stream)
+ {
+ var items = JsonSerializer.DeserializeAsyncEnumerable(stream, SerializerOptions);
+
+ var playerInfos = new Dictionary();
+
+ await foreach (var item in items)
+ {
+ if (item is null || item.Type == "markers") continue;
+
+ if (item.Type == "positions_infantry")
+ {
+ List units;
+ try
+ {
+ units = JsonSerializer.Deserialize>(item.Value, SerializerOptions) ?? new List();
+ }
+ catch (Exception)
+ {
+ continue;
+ }
+
+ var players = units?.Where(x => x.Id != "0").ToArray();
+ foreach (var player in players ?? Array.Empty())
+ {
+ var success = playerInfos.TryGetValue(player.Id, out _);
+ if (success)
+ {
+ continue;
+ }
+ else
+ {
+ var nameMatch = Regex.Match(player.Unit, @"\(.*\)");
+ var playerName = nameMatch.Success
+ ? nameMatch.Value.Trim('(').Trim(')')
+ : player.Unit;
+ var uidParseSuccess = ulong.TryParse(player.Id, out var playerUid);
+
+ if (!uidParseSuccess) continue;
+
+ playerInfos.Add(player.Id, new PlayerInfo
+ {
+ Name = playerName,
+ SteamUid = uidParseSuccess ? playerUid : 0
+ });
+ }
+ }
+ }
+ else
+ {
+ if (!KnownUselessEventTypes.Contains(item.Type))
+ {
+ KnownUselessEventTypes.Add(item.Type);
+ }
+ }
+ }
+
+ // var buffer = new byte[4096];
+ // _ = stream.Read(buffer);
+ // var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
+ // Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
+ // GetMoreBytesFromStream(stream, ref buffer, ref reader);
+ // if (reader.TokenType == JsonTokenType.StartObject)
+ // {
+ // JsonSerializer.Deserialize(reader.);
+ // }
+
+ return playerInfos.Values.ToList();
+ }
+
+ private static void GetMoreBytesFromStream(
+ Stream stream, ref byte[] buffer, ref Utf8JsonReader reader)
+ {
+ int bytesRead;
+ if (reader.BytesConsumed < buffer.Length)
+ {
+ ReadOnlySpan leftover = buffer.AsSpan((int)reader.BytesConsumed);
+
+ if (leftover.Length == buffer.Length)
+ {
+ Array.Resize(ref buffer, buffer.Length * 2);
+ Console.WriteLine($"Increased buffer size to {buffer.Length}");
+ }
+
+ leftover.CopyTo(buffer);
+ bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
+ }
+ else
+ {
+ bytesRead = stream.Read(buffer);
+ }
+ Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
+ reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
+ }
+}
diff --git a/ArmaforcesMissionBot.Tests/ArmaforcesMissionBot.Tests.csproj b/ArmaforcesMissionBot.Tests/ArmaforcesMissionBot.Tests.csproj
deleted file mode 100644
index 62e7ef0..0000000
--- a/ArmaforcesMissionBot.Tests/ArmaforcesMissionBot.Tests.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- netcoreapp3.1
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ArmaforcesMissionBot.Tests/Features/Signups/Importer/SignupImporterTests.cs b/ArmaforcesMissionBot.Tests/Features/Signups/Importer/SignupImporterTests.cs
deleted file mode 100644
index b17a552..0000000
--- a/ArmaforcesMissionBot.Tests/Features/Signups/Importer/SignupImporterTests.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using ArmaforcesMissionBot.Exceptions;
-using ArmaforcesMissionBot.Features;
-using ArmaforcesMissionBot.Features.Signups.Importer;
-using Discord.Commands;
-using FluentAssertions;
-using Moq;
-using Xunit;
-
-namespace ArmaforcesMissionBot.Tests.Features.Signups.Importer {
- public class SignupImporterTests {
- [Theory]
- [InlineData("Ddd\ndsd", "\n")]
- [InlineData("Ddd\r\ndsd", "\r\n")]
- [InlineData("Ddddsd", "\n")]
- public void GetLineEndings(string message, string expectedLinebreak) {
- var lineEnding = SignupImporter.GetLineEnding(message);
-
- lineEnding.Should().Be(expectedLinebreak);
- }
-
- [Theory]
- [InlineData("Ddd\ndsds\n", new[] {"Ddd\n", "dsds\n", "\n"})]
- [InlineData("Ddd\r\ndsds\r\n", new[] {"Ddd\r\n", "dsds\r\n", "\r\n"})]
- [InlineData("", new[] {"\n"})]
- public void ReadLines(string message, string[] expectedLines) {
- var linesRead = SignupImporter.ReadLines(message);
-
- linesRead.Should().BeEquivalentTo(expectedLines);
- }
-
- [Theory]
- [InlineData(new[] {"dsdsd", "AF!hsda", "#dsds", "asdas"}, new[] { "hsda\nasdas" })]
- [InlineData(new[] {"AF!dsdsd", "AF!hsda", "#dsds", "asdas"}, new[] { "dsdsd", "hsda\nasdas" })]
- [InlineData(new[] {"AF!dsdsd", "hsda", "#AF!dsds", "asdas"}, new[] { "dsdsd\nhsda\nasdas" })]
- [InlineData(new[] {"AF!dsdsd", "", "asdas"}, new[] { "dsdsd\n\nasdas" })]
- [InlineData(new[] {"dsdsd", "hsda", "#AF!dsds", "asdas"}, new string[0])]
- [InlineData(new[] {"dsdsd", "hsda", "//AF!dsds", "asdas"}, new string[0])]
- public void ParseCommands(IEnumerable lines, IEnumerable expectedCommandsList) {
- var parsedCommands = (IEnumerable) SignupImporter.ParseCommands(lines);
-
- parsedCommands.Should().BeEquivalentTo(expectedCommandsList);
- }
-
- [Theory]
- [InlineData("zrub-misje dsds", "zrub-misje")]
- [InlineData("zrub-misjeddds", "")]
- public void GetCommandName(string command, string expectedCommandName) {
- var commandName = SignupImporter.GetCommandName(command);
-
- commandName.Should().Be(expectedCommandName);
- }
-
- [Theory]
- [InlineData("zrub-misje dsadsa", "zrub-misje", "dsadsa")]
- [InlineData("zrub-misje asdasda \n\t", "zrub-misje", "asdasda")]
- public void GetParameterString(
- string command,
- string commandName,
- string expecterParameterString
- ) {
- var parameterString = SignupImporter.GetParameterString(command, commandName);
-
- parameterString.Should().Be(expecterParameterString);
- }
-
- [Fact]
- public void GetCommandInfoByName_NameEmpty_CommandInfoIsNull() {
- var signupImporter = PrepareImporter();
-
- var commandInfo = signupImporter.GetCommandInfoByName("");
-
- commandInfo.Should().BeNull();
- }
-
- [Fact]
- public async Task ProcessMessage_CommandsNotParsed_ThrowsInvalidParametersException() {
- var moduleMock = new Mock();
- var signupImporter = PrepareImporter(module: moduleMock.Object);
-
- await signupImporter.ProcessMessage("");
-
- moduleMock.Verify(x => x.ReplyWithException("Nie udało się odczytać komend."), Times.Once);
- }
-
- [Fact]
- public void ProcessCommand_CommandNotFound_ThrowsCommandNotFoundException() {
- const string commandName = "dsds ";
-
- var moduleMock = new Mock();
- moduleMock.Setup(x => x.ReplyWithException(It.IsAny()))
- .ThrowsAsync(new CommandNotFound($"Nie znaleziono komendy {commandName}"));
-
- var signupImporter = PrepareImporter(
- module: moduleMock.Object);
-
- Func task = async () => await signupImporter.ProcessCommand(commandName);
-
- task.Should().ThrowExactly($"Nie znaleziono komendy {commandName}");
- }
-
- private static SignupImporter PrepareImporter(
- ICommandContext commandContext = null,
- CommandService commandService = null,
- IServiceProvider serviceProvider = null,
- IModule module = null) {
-
- commandContext ??= new Mock().Object;
- commandService ??= new CommandService();
- serviceProvider ??= new Mock().Object;
- module ??= new Mock().Object;
-
- return new SignupImporter(
- commandContext,
- commandService,
- serviceProvider,
- module);
- }
- }
-}
diff --git a/ArmaforcesMissionBot.sln b/ArmaforcesMissionBot.sln
deleted file mode 100644
index a466b7d..0000000
--- a/ArmaforcesMissionBot.sln
+++ /dev/null
@@ -1,43 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28729.10
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArmaforcesMissionBotWeb", "ArmaforcesMissionBotWeb\ArmaforcesMissionBotWeb.csproj", "{F58CF544-6ED7-4468-A681-5CDAE3F60AE4}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArmaforcesMissionBotSharedClasses", "ArmaforcesMissionBotSharedClasses\ArmaforcesMissionBotSharedClasses.csproj", "{3C406BBC-0631-4F73-BADB-4D6DB3240376}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArmaforcesMissionBot", "ArmaforcesMissionBot\ArmaforcesMissionBot.csproj", "{9BD71946-1FB7-4461-9FFA-B20085B5B663}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArmaforcesMissionBot.Tests", "ArmaforcesMissionBot.Tests\ArmaforcesMissionBot.Tests.csproj", "{AE3D33CC-1515-47D3-AA67-CB285DBBA9D2}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {F58CF544-6ED7-4468-A681-5CDAE3F60AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F58CF544-6ED7-4468-A681-5CDAE3F60AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F58CF544-6ED7-4468-A681-5CDAE3F60AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F58CF544-6ED7-4468-A681-5CDAE3F60AE4}.Release|Any CPU.Build.0 = Release|Any CPU
- {3C406BBC-0631-4F73-BADB-4D6DB3240376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3C406BBC-0631-4F73-BADB-4D6DB3240376}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3C406BBC-0631-4F73-BADB-4D6DB3240376}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3C406BBC-0631-4F73-BADB-4D6DB3240376}.Release|Any CPU.Build.0 = Release|Any CPU
- {9BD71946-1FB7-4461-9FFA-B20085B5B663}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9BD71946-1FB7-4461-9FFA-B20085B5B663}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9BD71946-1FB7-4461-9FFA-B20085B5B663}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9BD71946-1FB7-4461-9FFA-B20085B5B663}.Release|Any CPU.Build.0 = Release|Any CPU
- {AE3D33CC-1515-47D3-AA67-CB285DBBA9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AE3D33CC-1515-47D3-AA67-CB285DBBA9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AE3D33CC-1515-47D3-AA67-CB285DBBA9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AE3D33CC-1515-47D3-AA67-CB285DBBA9D2}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {CC586110-52E4-477E-ACB2-36C2E476B426}
- EndGlobalSection
-EndGlobal
diff --git a/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj b/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj
deleted file mode 100644
index b0061ec..0000000
--- a/ArmaforcesMissionBot/ArmaforcesMissionBot.csproj
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- netcoreapp3.1
- InProcess
-
-
-
-
- DEBUG;TRACE
- full
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ArmaforcesMissionBot/Attributes/ContextDMOrChannelAttribute.cs b/ArmaforcesMissionBot/Attributes/ContextDMOrChannelAttribute.cs
deleted file mode 100644
index b4fdd96..0000000
--- a/ArmaforcesMissionBot/Attributes/ContextDMOrChannelAttribute.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using ArmaforcesMissionBot.DataClasses;
-using Discord;
-using Discord.Commands;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using System.Threading.Tasks;
-
-namespace ArmaforcesMissionBot.Attributes
-{
- [AttributeUsage(AttributeTargets.Method)]
- public class ContextDMOrChannelAttribute : PreconditionAttribute
- {
- public async override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
- {
- var config = services.GetService();
- if (context.Channel is IDMChannel || context.Channel.Id == config.CreateMissionChannel)
- return PreconditionResult.FromSuccess();
- else
- return PreconditionResult.FromError("Nie ten kanał");
- }
- }
-}
diff --git a/ArmaforcesMissionBot/Attributes/RequireRankAttribute.cs b/ArmaforcesMissionBot/Attributes/RequireRankAttribute.cs
deleted file mode 100644
index db9d83b..0000000
--- a/ArmaforcesMissionBot/Attributes/RequireRankAttribute.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using ArmaforcesMissionBot.DataClasses;
-using Discord.Commands;
-using Discord.WebSocket;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace ArmaforcesMissionBot.Attributes
-{
- public enum RanksEnum
- {
- Recruiter
- }
-
- internal static class RanksEnumMethods
- {
- public static ulong GetID(this RanksEnum role, Config config)
- => role switch
- {
- RanksEnum.Recruiter => config.RecruiterRole,
- _ => 0
- };
- }
-
- [AttributeUsage(AttributeTargets.Method)]
- public class RequireRankAttribute : PreconditionAttribute
- {
- private readonly RanksEnum _role;
-
- public RequireRankAttribute(RanksEnum role)
- {
- _role = role;
- }
-
- public async override Task CheckPermissionsAsync(
- ICommandContext context,
- CommandInfo command,
- IServiceProvider services)
- {
- var config = services.GetService();
-
- return ((SocketGuildUser) context.User).Roles.Any(x => x.Id == _role.GetID(config))
- ? PreconditionResult.FromSuccess()
- : PreconditionResult.FromError("Co ty próbujesz osiągnąć?");
- }
- }
-}
diff --git a/ArmaforcesMissionBot/Controllers/ApiController.cs b/ArmaforcesMissionBot/Controllers/ApiController.cs
deleted file mode 100644
index 7aff02e..0000000
--- a/ArmaforcesMissionBot/Controllers/ApiController.cs
+++ /dev/null
@@ -1,368 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using ArmaforcesMissionBot.DataClasses;
-using ArmaforcesMissionBot.Helpers;
-using ArmaforcesMissionBotSharedClasses;
-using Discord;
-using Discord.WebSocket;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace ArmaforcesMissionBot.Controllers
-{
- [Route("api/")]
- [ApiController]
- public class ApiController : ControllerBase
- {
- private readonly DiscordSocketClient _client;
- private readonly Config _config;
- private readonly MissionsArchiveData _missionsArchiveData;
- private readonly SignupsData _signupsData;
- private readonly BanHelper _banHelper;
- private readonly SignupHelper _signupHelper;
- private readonly MiscHelper _miscHelper;
-
- public ApiController(
- MissionsArchiveData missionsArchiveData,
- SignupsData signupsData,
- DiscordSocketClient client,
- BanHelper banHelper,
- SignupHelper signupHelper,
- MiscHelper miscHelper)
- {
- _missionsArchiveData = missionsArchiveData;
- _signupsData = signupsData;
- _client = client;
- _banHelper = banHelper;
- _signupHelper = signupHelper;
- _miscHelper = miscHelper;
- }
-
- [HttpGet("currentMission")]
- public IActionResult CurrentMission()
- {
- var mission = _signupsData.Missions
- .Concat(_missionsArchiveData.ArchiveMissions.Cast())
- .Where(mission => mission.Date < DateTime.Now && mission.Date.AddHours(3) > DateTime.Now)
- .OrderBy(mission => mission.Date)
- .FirstOrDefault();
-
- return mission is null
- ? (IActionResult) NotFound("No ongoing mission.")
- : Ok(mission);
- }
-
- [HttpGet("missions")]
- public void Missions(DateTime? fromDateTime = null, DateTime? toDateTime = null, bool includeArchive = false, uint ttl = 0)
- {
- fromDateTime = fromDateTime ?? DateTime.MinValue;
- toDateTime = toDateTime ?? DateTime.MaxValue;
-
- toDateTime = toDateTime ==
- new DateTime(toDateTime.Value.Year, toDateTime.Value.Month, toDateTime.Value.Day, 0, 0, 0)
- ? new DateTime(toDateTime.Value.Year,
- toDateTime.Value.Month,
- toDateTime.Value.Day,
- 23,
- 59,
- 59)
- : toDateTime;
-
- JArray missionArray = new JArray();
- var openMissionsEnumerable = _signupsData.Missions
- .Where(x => x.Editing == ArmaforcesMissionBotSharedClasses.Mission.EditEnum.NotEditing)
- .Where(x => x.Date >= fromDateTime)
- .Where(x => x.Date <= toDateTime)
- .Reverse();
- foreach (var mission in openMissionsEnumerable)
- {
- var objMission = new JObject();
- objMission.Add("title", mission.Title);
- objMission.Add("date", mission.Date.ToString("yyyy-MM-ddTHH:mm:ss"));
- objMission.Add("closeDate", mission.CloseTime.Value.ToString("yyyy-MM-ddTHH:mm:ss"));
- objMission.Add("image", mission.Attachment);
- objMission.Add("description", mission.Description);
- objMission.Add("modlist", mission.Modlist);
- objMission.Add("modlistName", mission.ModlistName);
- objMission.Add("modlistUrl", mission.ModlistUrl);
- objMission.Add("id", mission.SignupChannel);
- objMission.Add("freeSlots", Helpers.MiscHelper.CountFreeSlots(mission));
- objMission.Add("allSlots", Helpers.MiscHelper.CountAllSlots(mission));
- objMission.Add("state", "Open");
-
- missionArray.Add(objMission);
- }
-
- if(includeArchive)
- {
- var archiveMissionsEnumerable = _missionsArchiveData.ArchiveMissions.AsEnumerable()
- .Where(x => x.Date >= fromDateTime)
- .Where(x => x.Date <= toDateTime)
- .Reverse();
- foreach (var mission in archiveMissionsEnumerable)
- {
- var objMission = new JObject();
- objMission.Add("title", mission.Title);
- objMission.Add("date", mission.Date.ToString("yyyy-MM-ddTHH:mm:ss"));
- objMission.Add("closeDate", mission.CloseTime.Value.ToString("yyyy-MM-ddTHH:mm:ss"));
- objMission.Add("image", mission.Attachment);
- objMission.Add("description", mission.Description);
- objMission.Add("modlist", mission.Modlist);
- objMission.Add("modlistName", mission.ModlistName);
- objMission.Add("modlistUrl", mission.ModlistUrl);
- objMission.Add("archive", true);
- objMission.Add("freeSlots", mission.FreeSlots);
- objMission.Add("allSlots", mission.AllSlots);
- objMission.Add("state", mission.Date < DateTime.Now ? "Archived" : "Closed");
-
- missionArray.Add(objMission);
- }
- }
-
- Response.ContentType = "application/json; charset=utf-8";
- if (ttl != 0)
- {
- Response.Headers.Add("Cache-Control", $"public, max-age={ttl}");
- }
-
- Response.WriteAsync($"{missionArray.ToString()}");
- }
-
- [HttpGet("mission")]
- public void Mission(ulong id, ulong userID)
- {
- if (!_banHelper.IsUserSpamBanned(userID) && _signupHelper.ShowMissionToUser(userID, id))
- {
- var mission = _signupsData.Missions.Single(x => x.SignupChannel == id);
-
- var serialized = JsonConvert.SerializeObject(mission);
- Response.WriteAsync($"{serialized}");
- }
- else
- {
- Response.StatusCode = 503;
- Response.WriteAsync("Banned");
- }
- }
-
- [HttpGet("signup")]
- public async Task Signup(ulong missionID, ulong teamID, ulong userID, string slotID)
- {
- _signupsData.BanAccess.Wait(-1);
- try
- {
- if (_signupsData.SignupBans.ContainsKey(userID) ||
- _signupsData.SpamBans.ContainsKey(userID))
- {
- Response.StatusCode = 503;
- await Response.WriteAsync("Banned");
- return;
- }
- }
- finally
- {
- _signupsData.BanAccess.Release();
- }
-
- if (_signupsData.Missions.Any(x => x.SignupChannel == missionID))
- {
- var mission = _signupsData.Missions.Single(x => x.SignupChannel == missionID);
-
- mission.Access.Wait(-1);
- try
- {
- if (!mission.SignedUsers.Contains(userID))
- {
- if (mission.Teams.Any(x => x.TeamMsg == teamID))
- {
- var team = mission.Teams.Single(x => x.TeamMsg == teamID);
-
- if (team.Slots.Any(x => x.Emoji == slotID && x.Count > x.Signed.Count()))
- {
- var channel = _client.GetGuild(_config.AFGuild).GetTextChannel(missionID);
- var teamMsg = await channel.GetMessageAsync(teamID) as IUserMessage;
-
- var embed = teamMsg.Embeds.Single();
-
- if (!mission.SignedUsers.Contains(userID))
- {
- var slot = team.Slots.Single(x => x.Emoji == slotID);
- slot.Signed.Add(userID);
- mission.SignedUsers.Add(userID);
-
- var newDescription = _miscHelper.BuildTeamSlots(team);
-
- var newEmbed = new EmbedBuilder
- {
- Title = embed.Title,
- Color = embed.Color
- };
-
- if (newDescription.Count == 2)
- newEmbed.WithDescription(newDescription[0] + newDescription[1]);
- else if (newDescription.Count == 1)
- newEmbed.WithDescription(newDescription[0]);
-
- if (embed.Footer.HasValue)
- newEmbed.WithFooter(embed.Footer.Value.Text);
-
- await teamMsg.ModifyAsync(x => x.Embed = newEmbed.Build());
- await Response.WriteAsync("Success");
- return;
- }
- }
- }
- }
- }
- finally
- {
- mission.Access.Release();
- }
- }
-
- Response.StatusCode = 400;
- await Response.WriteAsync("Data invalid");
- }
-
- [HttpGet("signoff")]
- public async Task Signoff(ulong missionID, ulong teamID, ulong userID, string slotID)
- {
- _signupsData.BanAccess.Wait(-1);
- try
- {
- if (_signupsData.SignupBans.ContainsKey(userID) ||
- _signupsData.SpamBans.ContainsKey(userID))
- {
- Response.StatusCode = 503;
- await Response.WriteAsync("Banned");
- return;
- }
- }
- finally
- {
- _signupsData.BanAccess.Release();
- }
-
- if (_signupsData.Missions.Any(x => x.SignupChannel == missionID))
- {
- var mission = _signupsData.Missions.Single(x => x.SignupChannel == missionID);
-
- mission.Access.Wait(-1);
- try
- {
- if (mission.SignedUsers.Contains(userID))
- {
- if (mission.Teams.Any(x => x.TeamMsg == teamID))
- {
- var team = mission.Teams.Single(x => x.TeamMsg == teamID);
-
- if (team.Slots.Any(x => x.Emoji == slotID))
- {
- var channel = _client.GetGuild(_config.AFGuild).GetTextChannel(missionID);
- var teamMsg = await channel.GetMessageAsync(teamID) as IUserMessage;
-
- var embed = teamMsg.Embeds.Single();
-
- if (mission.SignedUsers.Contains(userID))
- {
- var slot = team.Slots.Single(x => x.Emoji == slotID);
- slot.Signed.Remove(userID);
- mission.SignedUsers.Remove(userID);
-
- var newDescription = _miscHelper.BuildTeamSlots(team);
-
- var newEmbed = new EmbedBuilder
- {
- Title = embed.Title,
- Color = embed.Color
- };
-
- if (newDescription.Count == 2)
- newEmbed.WithDescription(newDescription[0] + newDescription[1]);
- else if (newDescription.Count == 1)
- newEmbed.WithDescription(newDescription[0]);
-
- if (embed.Footer.HasValue)
- newEmbed.WithFooter(embed.Footer.Value.Text);
-
- await teamMsg.ModifyAsync(x => x.Embed = newEmbed.Build());
- await Response.WriteAsync("Success");
- return;
- }
- }
- }
- }
- }
- finally
- {
- mission.Access.Release();
- }
- }
-
- Response.StatusCode = 400;
- await Response.WriteAsync("Data invalid");
- }
-
- [HttpGet("emotes")]
- public void Emotes()
- {
- var emotes = _client.GetGuild(_config.AFGuild).Emotes;
- JArray emotesArray = new JArray();
- foreach (var emote in emotes)
- {
- var emoteObj = new JObject();
- var animated = emote.Animated ? "a" : "";
- emoteObj.Add("id", $"<{animated}:{emote.Name}:{emote.Id}>");
- emoteObj.Add("url", emote.Url);
-
- emotesArray.Add(emoteObj);
- }
- Response.WriteAsync($"{emotesArray.ToString()}");
- }
-
- [HttpGet("users")]
- public void Users()
- {
- var guild = _client.GetGuild(_config.AFGuild);
- var users = guild.Users;
- var makerRole = guild.GetRole(_config.MissionMakerRole);
- JArray usersArray = new JArray();
- foreach (var user in users)
- {
- var userObj = new JObject();
- userObj.Add("id", user.Id);
- userObj.Add("name", user.Username);
- userObj.Add("isMissionMaker", user.Roles.Contains(makerRole));
-
- usersArray.Add(userObj);
- }
- Response.WriteAsync($"{usersArray.ToString()}");
- }
-
- [HttpPost("createMission")]
- public async Task CreateMissionAsync(Mission mission)
- {
- Console.WriteLine(JsonConvert.SerializeObject(mission));
-
- mission.Editing = ArmaforcesMissionBotSharedClasses.Mission.EditEnum.New;
- _signupsData.Missions.Add(mission);
-
- if (Helpers.SignupHelper.CheckMissionComplete(mission))
- {
- var guild = _client.GetGuild(_config.AFGuild);
-
- var signupChannel = await _signupHelper.CreateChannelForMission(guild, mission, _signupsData);
- mission.SignupChannel = signupChannel.Id;
-
- await _signupHelper.CreateMissionMessagesOnChannel(guild, mission, signupChannel);
- }
- else
- {
- await Response.WriteAsync($"Incorrect data");
- }
- }
- }
-}
diff --git a/ArmaforcesMissionBot/Controllers/HealthController.cs b/ArmaforcesMissionBot/Controllers/HealthController.cs
deleted file mode 100644
index e153388..0000000
--- a/ArmaforcesMissionBot/Controllers/HealthController.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Discord;
-using Discord.WebSocket;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace ArmaforcesMissionBot.Controllers
-{
- [Route("health")]
- [ApiController]
- public class HealthController
- {
- private readonly DiscordSocketClient _discordClient;
-
- public HealthController(DiscordSocketClient discordClient)
- {
- _discordClient = discordClient;
- }
-
- [HttpGet]
- public IActionResult IndexAction()
- {
- return _discordClient.ConnectionState == ConnectionState.Connected
- ? new OkResult()
- : new StatusCodeResult(StatusCodes.Status503ServiceUnavailable);
- }
- }
-}
\ No newline at end of file
diff --git a/ArmaforcesMissionBot/DataClasses/BotConstants.cs b/ArmaforcesMissionBot/DataClasses/BotConstants.cs
deleted file mode 100644
index 7345f70..0000000
--- a/ArmaforcesMissionBot/DataClasses/BotConstants.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace ArmaforcesMissionBot.DataClasses
-{
- public class BotConstants
- {
- public const string DISCORD_USER_URL_PREFIX = "https://discord.com/users/";
-
- }
-}
diff --git a/ArmaforcesMissionBot/DataClasses/Config.cs b/ArmaforcesMissionBot/DataClasses/Config.cs
deleted file mode 100644
index 4c9f370..0000000
--- a/ArmaforcesMissionBot/DataClasses/Config.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using dotenv.net;
-using Newtonsoft.Json;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Reflection;
-using System.Text;
-
-namespace ArmaforcesMissionBot.DataClasses
-{
- public class Config
- {
- public string DiscordToken { get; set; }
- public ulong SignupsCategory { get; set; }
- public ulong SignupsArchive { get; set; }
- public ulong AFGuild { get; set; }
- public ulong MissionMakerRole { get; set; }
- public ulong SignupRole { get; set; }
- public ulong BotRole { get; set; }
- public ulong RecruiterRole { get; set; }
- public ulong RecruitRole { get; set; }
- public string KickImageUrl { get; set; }
- public string ServerManagerUrl { get; set; }
- public string ServerManagerApiKey { get; set; }
- public string ModsetsApiUrl { get; set; }
- public ulong CreateMissionChannel { get; set; }
- public ulong PublicContemptChannel { get; set; }
- public ulong HallOfShameChannel { get; set; }
- public ulong RecruitInfoChannel { get; set; }
- public ulong RecruitAskChannel { get; set; }
-
- public void Load()
- {
- DotEnv.Config(false);
-
- PropertyInfo[] properties = typeof(Config).GetProperties(BindingFlags.Public | BindingFlags.Instance);
- foreach (var prop in properties)
- {
- if(prop.PropertyType == typeof(string))
- prop.SetValue(this, Environment.GetEnvironmentVariable("AF_" + prop.Name));
- if (prop.PropertyType == typeof(ulong))
- prop.SetValue(this, ulong.Parse(Environment.GetEnvironmentVariable("AF_" + prop.Name)));
- }
- }
- }
-}
diff --git a/ArmaforcesMissionBot/DataClasses/MissionsArchiveData.cs b/ArmaforcesMissionBot/DataClasses/MissionsArchiveData.cs
deleted file mode 100644
index 7f282be..0000000
--- a/ArmaforcesMissionBot/DataClasses/MissionsArchiveData.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using ArmaforcesMissionBotSharedClasses;
-
-namespace ArmaforcesMissionBot.DataClasses
-{
- public class MissionsArchiveData
- {
- public class Mission : IMission
- {
- public string Title { get; set; }
- public DateTime Date { get; set; }
- public DateTime? CloseTime { get; set; } = null;
- public string Description { get; set; }
- public string Modlist { get; set; }
- public string ModlistUrl;
- public string ModlistName;
- public string Attachment;
- public ulong FreeSlots { get; set; }
- public ulong AllSlots { get; set; }
- }
-
- public List ArchiveMissions = new List();
- }
-}
diff --git a/ArmaforcesMissionBot/DataClasses/OpenedDialogs.cs b/ArmaforcesMissionBot/DataClasses/OpenedDialogs.cs
deleted file mode 100644
index 3fa8daa..0000000
--- a/ArmaforcesMissionBot/DataClasses/OpenedDialogs.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace ArmaforcesMissionBot.DataClasses
-{
- public class OpenedDialogs
- {
- public class Dialog
- {
- public ulong DialogID = 0;
- public ulong DialogOwner = 0;
- public Dictionary> Buttons = new Dictionary>();
- }
-
- public List