From 92925d2d0e86e3790ef93a686e49a0a660abdbb0 Mon Sep 17 00:00:00 2001 From: Emotion Date: Tue, 11 Jul 2023 22:00:14 +1200 Subject: [PATCH] Added Ellie.VotesApi --- Ellie.sln | 24 +++-- src/Ellie.VotesApi/.dockerignore | 25 +++++ src/Ellie.VotesApi/.gitignore | 1 + src/Ellie.VotesApi/Common/AuthHandler.cs | 41 +++++++ src/Ellie.VotesApi/Common/ConfKeys.cs | 8 ++ .../Common/DiscordsVoteWebhookModel.cs | 26 +++++ src/Ellie.VotesApi/Common/Policies.cs | 8 ++ .../Common/ToppVoteWebhookModel.cs | 30 ++++++ .../Controllers/DiscordsController.cs | 33 ++++++ .../Controllers/TopGgController.cs | 34 ++++++ .../Controllers/WebhookController.cs | 48 +++++++++ src/Ellie.VotesApi/Dockerfile | 20 ++++ src/Ellie.VotesApi/Ellie.VotesApi.csproj | 13 +++ src/Ellie.VotesApi/Program.cs | 9 ++ .../Properties/launchSettings.json | 31 ++++++ src/Ellie.VotesApi/README.md | 46 ++++++++ src/Ellie.VotesApi/Services/FileVotesCache.cs | 100 ++++++++++++++++++ src/Ellie.VotesApi/Services/IVotesCache.cs | 13 +++ src/Ellie.VotesApi/Startup.cs | 68 ++++++++++++ src/Ellie.VotesApi/WeatherForecast.cs | 7 ++ .../appsettings.Development.json | 10 ++ src/Ellie.VotesApi/appsettings.json | 12 +++ 22 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 src/Ellie.VotesApi/.dockerignore create mode 100644 src/Ellie.VotesApi/.gitignore create mode 100644 src/Ellie.VotesApi/Common/AuthHandler.cs create mode 100644 src/Ellie.VotesApi/Common/ConfKeys.cs create mode 100644 src/Ellie.VotesApi/Common/DiscordsVoteWebhookModel.cs create mode 100644 src/Ellie.VotesApi/Common/Policies.cs create mode 100644 src/Ellie.VotesApi/Common/ToppVoteWebhookModel.cs create mode 100644 src/Ellie.VotesApi/Controllers/DiscordsController.cs create mode 100644 src/Ellie.VotesApi/Controllers/TopGgController.cs create mode 100644 src/Ellie.VotesApi/Controllers/WebhookController.cs create mode 100644 src/Ellie.VotesApi/Dockerfile create mode 100644 src/Ellie.VotesApi/Ellie.VotesApi.csproj create mode 100644 src/Ellie.VotesApi/Program.cs create mode 100644 src/Ellie.VotesApi/Properties/launchSettings.json create mode 100644 src/Ellie.VotesApi/README.md create mode 100644 src/Ellie.VotesApi/Services/FileVotesCache.cs create mode 100644 src/Ellie.VotesApi/Services/IVotesCache.cs create mode 100644 src/Ellie.VotesApi/Startup.cs create mode 100644 src/Ellie.VotesApi/WeatherForecast.cs create mode 100644 src/Ellie.VotesApi/appsettings.Development.json create mode 100644 src/Ellie.VotesApi/appsettings.json diff --git a/Ellie.sln b/Ellie.sln index 88d2626..8ad5067 100644 --- a/Ellie.sln +++ b/Ellie.sln @@ -6,13 +6,14 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0B2F1537-4BF0-422B-A0DD-8F9CCEFB340F}" -ProjectSection(SolutionItems) = preProject - CHANGELOG.md = CHANGELOG.md - LICENSE.md = LICENSE.md - README.md = README.md - Dockerfile = Dockerfile - NuGet.Config = NuGet.Config -EndProjectSection + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + Dockerfile = Dockerfile + LICENSE.md = LICENSE.md + NuGet.Config = NuGet.Config + README.md = README.md + EndProjectSection +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie", "src\Ellie\Ellie.csproj", "{2BAF005E-781D-45FF-B218-E6361F5E8CD4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ayu", "ayu", "{5284415D-A43F-4539-9483-410124199743}" @@ -25,7 +26,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Coordinator", "src\Ellie.Coordinator\Ellie.Coordinator.csproj", "{44BE7271-BABE-46BE-BB41-A5B6F1116C21}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Bot.Generators.Strings", "src\Ellie.Bot.Generators.Strings\Ellie.Bot.Generators.Strings.csproj", "{11DE9EB6-2793-4540-BE66-701D2D02903A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Strings", "src\Ellie.Bot.Generators.Strings\Ellie.Bot.Generators.Strings.csproj", "{11DE9EB6-2793-4540-BE66-701D2D02903A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.VotesApi", "src\Ellie.VotesApi\Ellie.VotesApi.csproj", "{8D996036-52D1-4B11-B7D7-6F853A907EDD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -57,6 +60,10 @@ Global {11DE9EB6-2793-4540-BE66-701D2D02903A}.Debug|Any CPU.Build.0 = Debug|Any CPU {11DE9EB6-2793-4540-BE66-701D2D02903A}.Release|Any CPU.ActiveCfg = Release|Any CPU {11DE9EB6-2793-4540-BE66-701D2D02903A}.Release|Any CPU.Build.0 = Release|Any CPU + {8D996036-52D1-4B11-B7D7-6F853A907EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D996036-52D1-4B11-B7D7-6F853A907EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +76,7 @@ Global {D6CF9ABE-205E-4699-90CA-0F18ED236490} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67} {44BE7271-BABE-46BE-BB41-A5B6F1116C21} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67} {11DE9EB6-2793-4540-BE66-701D2D02903A} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67} + {8D996036-52D1-4B11-B7D7-6F853A907EDD} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {878761F1-C7B5-4D38-A00D-3377D703EBBA} diff --git a/src/Ellie.VotesApi/.dockerignore b/src/Ellie.VotesApi/.dockerignore new file mode 100644 index 0000000..af50df1 --- /dev/null +++ b/src/Ellie.VotesApi/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Ellie.VotesApi/.gitignore b/src/Ellie.VotesApi/.gitignore new file mode 100644 index 0000000..9ae80d3 --- /dev/null +++ b/src/Ellie.VotesApi/.gitignore @@ -0,0 +1 @@ +store/ \ No newline at end of file diff --git a/src/Ellie.VotesApi/Common/AuthHandler.cs b/src/Ellie.VotesApi/Common/AuthHandler.cs new file mode 100644 index 0000000..14176e6 --- /dev/null +++ b/src/Ellie.VotesApi/Common/AuthHandler.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Ellie.VotesApi +{ + public class AuthHandler : AuthenticationHandler + { + public const string SchemeName = "AUTHORIZATION_SCHEME"; + public const string DiscordsClaim = "DISCORDS_CLAIM"; + public const string TopggClaim = "TOPGG_CLAIM"; + + private readonly IConfiguration _conf; + + public AuthHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IConfiguration conf) + : base(options, logger, encoder, clock) + => _conf = conf; + + protected override Task HandleAuthenticateAsync() + { + var claims = new List(); + + if (_conf[ConfKeys.DISCORDS_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim()) + claims.Add(new(DiscordsClaim, "true")); + + if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim()) + claims.Add(new Claim(TopggClaim, "true")); + + return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName))); + } + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/Common/ConfKeys.cs b/src/Ellie.VotesApi/Common/ConfKeys.cs new file mode 100644 index 0000000..9e81f46 --- /dev/null +++ b/src/Ellie.VotesApi/Common/ConfKeys.cs @@ -0,0 +1,8 @@ +namespace Ellie.VotesApi +{ + public static class ConfKeys + { + public const string DISCORDS_KEY = "DiscordsKey"; + public const string TOPGG_KEY = "TopGGKey"; + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/Common/DiscordsVoteWebhookModel.cs b/src/Ellie.VotesApi/Common/DiscordsVoteWebhookModel.cs new file mode 100644 index 0000000..cbdbc77 --- /dev/null +++ b/src/Ellie.VotesApi/Common/DiscordsVoteWebhookModel.cs @@ -0,0 +1,26 @@ +namespace Ellie.VotesApi +{ + public class DiscordsVoteWebhookModel + { + /// + /// The ID of the user who voted + /// + public string User { get; set; } + + /// + /// The ID of the bot which recieved the vote + /// + public string Bot { get; set; } + + /// + /// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and + /// Voted24 - a list of IDs of users who have voted today + /// + public string Votes { get; set; } + + /// + /// The type of event, whether it is a vote event or test event + /// + public string Type { get; set; } + } +} diff --git a/src/Ellie.VotesApi/Common/Policies.cs b/src/Ellie.VotesApi/Common/Policies.cs new file mode 100644 index 0000000..0395bf9 --- /dev/null +++ b/src/Ellie.VotesApi/Common/Policies.cs @@ -0,0 +1,8 @@ +namespace Ellie.VotesApi +{ + public static class Policies + { + public const string DiscordsAuth = "DiscordsAuth"; + public const string TopggAuth = "TopggAuth"; + } +} diff --git a/src/Ellie.VotesApi/Common/ToppVoteWebhookModel.cs b/src/Ellie.VotesApi/Common/ToppVoteWebhookModel.cs new file mode 100644 index 0000000..af30284 --- /dev/null +++ b/src/Ellie.VotesApi/Common/ToppVoteWebhookModel.cs @@ -0,0 +1,30 @@ +namespace Ellie.VotesApi +{ + public class TopggVoteWebhookModel + { + /// + /// Discord ID of the bot that received a vote. + /// + public string Bot { get; set; } + + /// + /// Discord ID of the user who voted. + /// + public string User { get; set; } + + /// + /// The type of the vote (should always be "upvote" except when using the test button it's "test"). + /// + public string Type { get; set; } + + /// + /// Whether the weekend multiplier is in effect, meaning users votes count as two. + /// + public bool Weekend { get; set; } + + /// + /// Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2. + /// + public string Query { get; set; } + } +} diff --git a/src/Ellie.VotesApi/Controllers/DiscordsController.cs b/src/Ellie.VotesApi/Controllers/DiscordsController.cs new file mode 100644 index 0000000..16ed6fa --- /dev/null +++ b/src/Ellie.VotesApi/Controllers/DiscordsController.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Ellie.VotesApi.Services; + +namespace Ellie.VotesApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DiscordsController : ControllerBase + { + private readonly ILogger _logger; + private readonly IVotesCache _cache; + + public DiscordsController(ILogger logger, IVotesCache cache) + { + _logger = logger; + _cache = cache; + } + + [HttpGet("new")] + [Authorize(Policy = Policies.DiscordsAuth)] + public async Task> New() + { + var votes = await _cache.GetNewDiscordsVotesAsync(); + if(votes.Count > 0) + _logger.LogInformation("Sending {NewDiscordsVotes} new discords votes", votes.Count); + return votes; + } + } +} diff --git a/src/Ellie.VotesApi/Controllers/TopGgController.cs b/src/Ellie.VotesApi/Controllers/TopGgController.cs new file mode 100644 index 0000000..da01b03 --- /dev/null +++ b/src/Ellie.VotesApi/Controllers/TopGgController.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Ellie.VotesApi.Services; + +namespace Ellie.VotesApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class TopGgController : ControllerBase + { + private readonly ILogger _logger; + private readonly IVotesCache _cache; + + public TopGgController(ILogger logger, IVotesCache cache) + { + _logger = logger; + _cache = cache; + } + + [HttpGet("new")] + [Authorize(Policy = Policies.TopggAuth)] + public async Task> New() + { + var votes = await _cache.GetNewTopGgVotesAsync(); + if (votes.Count > 0) + _logger.LogInformation("Sending {NewTopggVotes} new topgg votes", votes.Count); + + return votes; + } + } +} diff --git a/src/Ellie.VotesApi/Controllers/WebhookController.cs b/src/Ellie.VotesApi/Controllers/WebhookController.cs new file mode 100644 index 0000000..2a0702f --- /dev/null +++ b/src/Ellie.VotesApi/Controllers/WebhookController.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Ellie.VotesApi.Services; + +namespace Ellie.VotesApi.Controllers +{ + [ApiController] + public class WebhookController : ControllerBase + { + private readonly ILogger _logger; + private readonly IVotesCache _votesCache; + + public WebhookController(ILogger logger, IVotesCache votesCache) + { + _logger = logger; + _votesCache = votesCache; + } + + [HttpPost("/discordswebhook")] + [Authorize(Policy = Policies.DiscordsAuth)] + public async Task DiscordsWebhook([FromBody] DiscordsVoteWebhookModel data) + { + + _logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}", + data.User, + data.Bot, + "discords.com"); + + await _votesCache.AddNewDiscordsVote(data.User); + return Ok(); + } + + [HttpPost("/topggwebhook")] + [Authorize(Policy = Policies.TopggAuth)] + public async Task TopggWebhook([FromBody] TopggVoteWebhookModel data) + { + _logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}", + data.User, + data.Bot, + "top.gg"); + + await _votesCache.AddNewTopggVote(data.User); + return Ok(); + } + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/Dockerfile b/src/Ellie.VotesApi/Dockerfile new file mode 100644 index 0000000..579948f --- /dev/null +++ b/src/Ellie.VotesApi/Dockerfile @@ -0,0 +1,20 @@ +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 ["src/Ellie.VotesApi/Ellie.VotesApi.csproj", "Ellie.VotesApi/"] +RUN dotnet restore "src/Ellie.VotesApi/Ellie.VotesApi.csproj" +COPY . . +WORKDIR "/src/Ellie.VotesApi" +RUN dotnet build "Ellie.VotesApi.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Ellie.VotesApi.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Ellie.VotesApi.dll"] diff --git a/src/Ellie.VotesApi/Ellie.VotesApi.csproj b/src/Ellie.VotesApi/Ellie.VotesApi.csproj new file mode 100644 index 0000000..63ac99b --- /dev/null +++ b/src/Ellie.VotesApi/Ellie.VotesApi.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + Linux + + + + + + + + diff --git a/src/Ellie.VotesApi/Program.cs b/src/Ellie.VotesApi/Program.cs new file mode 100644 index 0000000..be5fd9a --- /dev/null +++ b/src/Ellie.VotesApi/Program.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Ellie.VotesApi; + +CreateHostBuilder(args).Build().Run(); + +static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); \ No newline at end of file diff --git a/src/Ellie.VotesApi/Properties/launchSettings.json b/src/Ellie.VotesApi/Properties/launchSettings.json new file mode 100644 index 0000000..6919caf --- /dev/null +++ b/src/Ellie.VotesApi/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16451", + "sslPort": 44323 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Ellie.VotesApi": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Ellie.VotesApi/README.md b/src/Ellie.VotesApi/README.md new file mode 100644 index 0000000..4c22299 --- /dev/null +++ b/src/Ellie.VotesApi/README.md @@ -0,0 +1,46 @@ +## Votes Api + +This api is used if you want your bot to be able to reward users who vote for it on discords.com or top.gg + +#### [GET] `/discords/new` + Get the discords votes received after previous call to this endpoint. + Input full url of this endpoint in your creds.yml file under Discords url field. + For example "https://api.my.cool.bot/discords/new" +#### [GET] `/topgg/new` + Get the topgg votes received after previous call to this endpoint. + Input full url of this endpoint in your creds.yml file under Topgg url field. + For example "https://api.my.cool.bot/topgg/new" + +#### [POST] `/discordswebhook` + Input this endpoint as the webhook on discords.com bot edit page + model: https://docs.botsfordiscord.com/methods/receiving-votes + For example "https://api.my.cool.bot/topggwebhook" +#### [POST] `/topggwebhook` + Input this endpoint as the webhook https://top.gg/bot/:your-bot-id/webhooks (replace :your-bot-id with your bot's id) + model: https://docs.top.gg/resources/webhooks/#schema + For example "https://api.my.cool.bot/discordswebhook" + +Input your super-secret header value in appsettings.json's DiscordsKey and TopGGKey fields +They must match your DiscordsKey and TopGG key respectively, as well as your secrets in the discords.com and top.gg webhook setup pages + +Full Example: + +⚠ Change TopggKey and DiscordsKey to a secure long string +⚠ You can use https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new to generate it + +`creds.yml` +```yml +votes: + TopggServiceUrl: "https://api.my.cool.bot/topgg" + TopggKey: "my_topgg_key" + DiscordsServiceUrl: "https://api.my.cool.bot/discords" + DiscordsKey: "my_discords_key" +``` + +`appsettings.json` +```json +... + "DiscordsKey": "my_discords_key", + "TopGGKey": "my_topgg_key", +... +``` \ No newline at end of file diff --git a/src/Ellie.VotesApi/Services/FileVotesCache.cs b/src/Ellie.VotesApi/Services/FileVotesCache.cs new file mode 100644 index 0000000..c9025f2 --- /dev/null +++ b/src/Ellie.VotesApi/Services/FileVotesCache.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using MorseCode.ITask; + +namespace Ellie.VotesApi.Services +{ + public class FileVotesCache : IVotesCache + { + // private const string STATS_FILE = "store/stats.json"; + private const string TOPGG_FILE = "store/topgg.json"; + private const string DISCORDS_FILE = "store/discords.json"; + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + public FileVotesCache() + { + if (!Directory.Exists("store")) + Directory.CreateDirectory("store"); + + if (!File.Exists(TOPGG_FILE)) + File.WriteAllText(TOPGG_FILE, "[]"); + + if (!File.Exists(DISCORDS_FILE)) + File.WriteAllText(DISCORDS_FILE, "[]"); + } + + public ITask AddNewTopggVote(string userId) + => AddNewVote(TOPGG_FILE, userId); + + public ITask AddNewDiscordsVote(string userId) + => AddNewVote(DISCORDS_FILE, userId); + + private async ITask AddNewVote(string file, string userId) + { + await _locker.WaitAsync(); + try + { + var votes = await GetVotesAsync(file); + votes.Add(userId); + await File.WriteAllTextAsync(file, JsonSerializer.Serialize(votes)); + } + finally + { + _locker.Release(); + } + } + + public async ITask> GetNewTopGgVotesAsync() + { + var votes = await EvictTopggVotes(); + return votes; + } + + public async ITask> GetNewDiscordsVotesAsync() + { + var votes = await EvictDiscordsVotes(); + return votes; + } + + private ITask> EvictTopggVotes() + => EvictVotes(TOPGG_FILE); + + private ITask> EvictDiscordsVotes() + => EvictVotes(DISCORDS_FILE); + + private async ITask> EvictVotes(string file) + { + await _locker.WaitAsync(); + try + { + + var ids = await GetVotesAsync(file); + await File.WriteAllTextAsync(file, "[]"); + + return ids? + .Select(x => (Ok: ulong.TryParse(x, out var r), Id: r)) + .Where(x => x.Ok) + .Select(x => new Vote + { + UserId = x.Id + }) + .ToList(); + } + finally + { + _locker.Release(); + } + } + + private async ITask> GetVotesAsync(string file) + { + await using var fs = File.Open(file, FileMode.Open); + var votes = await JsonSerializer.DeserializeAsync>(fs); + return votes; + } + } +} diff --git a/src/Ellie.VotesApi/Services/IVotesCache.cs b/src/Ellie.VotesApi/Services/IVotesCache.cs new file mode 100644 index 0000000..e43e06a --- /dev/null +++ b/src/Ellie.VotesApi/Services/IVotesCache.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using MorseCode.ITask; + +namespace Ellie.VotesApi.Services +{ + public interface IVotesCache + { + ITask> GetNewTopGgVotesAsync(); + ITask> GetNewDiscordsVotesAsync(); + ITask AddNewTopggVote(string userId); + ITask AddNewDiscordsVote(string userId); + } +} diff --git a/src/Ellie.VotesApi/Startup.cs b/src/Ellie.VotesApi/Startup.cs new file mode 100644 index 0000000..1a1a3f2 --- /dev/null +++ b/src/Ellie.VotesApi/Startup.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Ellie.VotesApi.Services; + +namespace Ellie.VotesApi +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + => Configuration = configuration; + + + // This method get called by the runtime. Use this method to add services tp the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSingleton(); + services.AddSwaggerGen(static c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Ellie.VotesApi", Version = "v1" }); + }); + + services + .AddAuthentication(opts => + { + opts.DefaultScheme = AuthHandler.SchemeName; + opts.AddScheme(AuthHandler.SchemeName, AuthHandler.SchemeName); + }); + + services + .AddAuthorization(static opts => + { + opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName) + .RequireAssertion(static _ => false) + .Build(); + opts.AddPolicy(Policies.DiscordsAuth, static policy => policy.RequireClaim(AuthHandler.DiscordsClaim)); + opts.AddPolicy(Policies.TopggAuth, static policy => policy.RequireClaim(AuthHandler.TopggClaim)); + }); + } + + // 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.UseSwagger(); + app.UseSwaggerUI(static c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ellie.VotesApi v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(static endpoints => { endpoints.MapControllers(); }); + } + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/WeatherForecast.cs b/src/Ellie.VotesApi/WeatherForecast.cs new file mode 100644 index 0000000..c59967e --- /dev/null +++ b/src/Ellie.VotesApi/WeatherForecast.cs @@ -0,0 +1,7 @@ +namespace Ellie.VotesApi +{ + public class Vote + { + public ulong UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/appsettings.Development.json b/src/Ellie.VotesApi/appsettings.Development.json new file mode 100644 index 0000000..9fca86a --- /dev/null +++ b/src/Ellie.VotesApi/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} \ No newline at end of file diff --git a/src/Ellie.VotesApi/appsettings.json b/src/Ellie.VotesApi/appsettings.json new file mode 100644 index 0000000..16a568a --- /dev/null +++ b/src/Ellie.VotesApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "DiscordsKey": "my_discords_key", + "TopGGKey": "my_topgg_key", + "AllowedHosts": "*" +} \ No newline at end of file