Refactor WebApi Controllers to take advantage of new .net6 features

This commit is contained in:
Daan Boerlage 2021-11-07 00:08:08 +01:00
parent 6b3a3a9ec2
commit e01a066920
Signed by: daan
GPG key ID: FCE070E1E4956606
21 changed files with 452 additions and 472 deletions

View file

@ -1,7 +1,6 @@
namespace Geekbot.Web namespace Geekbot.Web;
public record ApiError
{ {
public class ApiError public string Message { get; set; }
{
public string Message { get; set; }
}
} }

View file

@ -1,54 +1,50 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Geekbot.Core.GlobalSettings; using Geekbot.Core.GlobalSettings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Geekbot.Web.Controllers.Callback namespace Geekbot.Web.Controllers.Callback;
[ApiController]
public class CallbackController : ControllerBase
{ {
public class CallbackController : Controller private readonly IGlobalSettings _globalSettings;
public CallbackController(IGlobalSettings globalSettings)
{ {
private readonly IGlobalSettings _globalSettings; _globalSettings = globalSettings;
}
public CallbackController(IGlobalSettings globalSettings) [Route("/callback")]
public async Task<IActionResult> DoCallback([FromQuery] string code)
{
var token = "";
using (var client = new HttpClient())
{ {
_globalSettings = globalSettings; client.BaseAddress = new Uri("https://discordapp.com");
} var appId = _globalSettings.GetKey("DiscordApplicationId");
var accessToken = _globalSettings.GetKey("OAuthToken");
[Route("/callback")] var callbackUrl = _globalSettings.GetKey("OAuthCallbackUrl");
public async Task<IActionResult> DoCallback([FromQuery] string code)
{ var form = new Dictionary<string, string>
var token = "";
using (var client = new HttpClient())
{ {
client.BaseAddress = new Uri("https://discordapp.com"); { "client_id", appId },
var appId = _globalSettings.GetKey("DiscordApplicationId"); { "client_secret", accessToken },
var accessToken = _globalSettings.GetKey("OAuthToken"); { "grant_type", "authorization_code" },
var callbackUrl = _globalSettings.GetKey("OAuthCallbackUrl"); { "code", code },
{ "scope", "identify email guilds" },
{ "redirect_uri", callbackUrl }
};
var form = new Dictionary<string, string> client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
{ var result = await client.PostAsync("/api/oauth2/token", new FormUrlEncodedContent(form));
{"client_id", appId}, result.EnsureSuccessStatusCode();
{"client_secret", accessToken},
{"grant_type", "authorization_code"},
{"code", code},
{"scope", "identify email guilds"},
{"redirect_uri", callbackUrl}
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded")); var stringResponse = await result.Content.ReadAsStringAsync();
var result = await client.PostAsync("/api/oauth2/token", new FormUrlEncodedContent(form)); var responseData = JsonSerializer.Deserialize<CallbackTokenResponse>(stringResponse);
result.EnsureSuccessStatusCode(); token = responseData.AccessToken;
var stringResponse = await result.Content.ReadAsStringAsync();
var responseData = JsonSerializer.Deserialize<CallbackTokenResponseDto>(stringResponse);
token = responseData.AccessToken;
}
return new RedirectResult($"https://geekbot.pizzaandcoffee.rocks/login?token={token}", false);
} }
return new RedirectResult($"https://geekbot.pizzaandcoffee.rocks/login?token={token}", false);
} }
} }

View file

@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace Geekbot.Web.Controllers.Callback;
public record CallbackTokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
}

View file

@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
namespace Geekbot.Web.Controllers.Callback
{
public class CallbackTokenResponseDto
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
}
}

View file

@ -1,41 +1,40 @@
using System.Linq; using Discord.Commands;
using Discord.Commands;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Geekbot.Web.Controllers.Commands namespace Geekbot.Web.Controllers.Commands;
{
[EnableCors("AllowSpecificOrigin")]
public class CommandController : Controller
{
private readonly CommandService _commands;
public CommandController(CommandService commands) [ApiController]
{ [EnableCors("AllowSpecificOrigin")]
_commands = commands; public class CommandController : ControllerBase
} {
private readonly CommandService _commands;
[Route("/v1/commands")]
public IActionResult GetCommands() public CommandController(CommandService commands)
{ {
var commandList = (from cmd in _commands.Commands _commands = commands;
let cmdParamsObj = cmd.Parameters.Select(cmdParam => new CommandParamDto }
{
Summary = cmdParam.Summary, [HttpGet("/v1/commands")]
Default = cmdParam.DefaultValue?.ToString(), public IActionResult GetCommands()
Type = cmdParam.Type?.ToString() {
}) var commandList = (from cmd in _commands.Commands
.ToList() let cmdParamsObj = cmd.Parameters.Select(cmdParam => new ResponseCommandParam
let param = string.Join(", !", cmd.Aliases)
select new CommandDto
{ {
Name = cmd.Name, Summary = cmdParam.Summary,
Summary = cmd.Summary, Default = cmdParam.DefaultValue?.ToString(),
IsAdminCommand = param.Contains("admin") || param.Contains("owner"), Type = cmdParam.Type?.ToString()
Aliases = cmd.Aliases.ToList(), })
Params = cmdParamsObj .ToList()
}).ToList(); let param = string.Join(", !", cmd.Aliases)
return Ok(commandList.FindAll(e => !e.Aliases[0].StartsWith("owner"))); select new ResponseCommand
} {
Name = cmd.Name,
Summary = cmd.Summary,
IsAdminCommand = param.Contains("admin") || param.Contains("owner"),
Aliases = cmd.Aliases.ToList(),
Params = cmdParamsObj
}).ToList();
return Ok(commandList.FindAll(e => !e.Aliases[0].StartsWith("owner")));
} }
} }

View file

@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace Geekbot.Web.Controllers.Commands
{
public class CommandDto
{
public string Name { get; set; }
public string Summary { get; set; }
public bool IsAdminCommand { get; set; }
public List<string> Aliases { get; set; }
public List<CommandParamDto> Params { get; set; }
}
}

View file

@ -1,9 +0,0 @@
namespace Geekbot.Web.Controllers.Commands
{
public class CommandParamDto
{
public string Summary { get; set; }
public string Default { get; set; }
public string Type { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Geekbot.Web.Controllers.Commands;
public record ResponseCommand
{
public string Name { get; set; }
public string Summary { get; set; }
public bool IsAdminCommand { get; set; }
public List<string> Aliases { get; set; }
public List<ResponseCommandParam> Params { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace Geekbot.Web.Controllers.Commands;
public record ResponseCommandParam
{
public string Summary { get; set; }
public string Default { get; set; }
public string Type { get; set; }
}

View file

@ -1,56 +1,56 @@
using System.Collections.Generic;
using Geekbot.Core.Highscores; using Geekbot.Core.Highscores;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Geekbot.Web.Controllers.Highscores namespace Geekbot.Web.Controllers.Highscores;
[ApiController]
[EnableCors("AllowSpecificOrigin")]
public class HighscoreController : ControllerBase
{ {
[EnableCors("AllowSpecificOrigin")] private readonly IHighscoreManager _highscoreManager;
public class HighscoreController : Controller
public HighscoreController(IHighscoreManager highscoreManager)
{ {
private readonly IHighscoreManager _highscoreManager; _highscoreManager = highscoreManager;
}
public HighscoreController(IHighscoreManager highscoreManager) [HttpPost]
[Route("/v1/highscore")]
public IActionResult GetHighscores([FromBody] HighscoreControllerPostBody body)
{
if (!ModelState.IsValid || body == null)
{ {
_highscoreManager = highscoreManager; var error = new SerializableError(ModelState);
return BadRequest(error);
} }
[HttpPost] Dictionary<HighscoreUserDto, int> list;
[Route("/v1/highscore")] try
public IActionResult GetHighscores([FromBody] HighscoreControllerPostBodyDto body)
{ {
if (!ModelState.IsValid || body == null) list = _highscoreManager.GetHighscoresWithUserData(body.Type, body.GuildId, body.Amount);
{
var error = new SerializableError(ModelState);
return BadRequest(error);
}
Dictionary<HighscoreUserDto, int> list;
try
{
list = _highscoreManager.GetHighscoresWithUserData(body.Type, body.GuildId, body.Amount);
}
catch (HighscoreListEmptyException)
{
return NotFound(new ApiError
{
Message = $"No {body.Type} found on this server"
});
}
var response = new List<HighscoreControllerReponseBody>();
var counter = 1;
foreach (var item in list)
{
response.Add(new HighscoreControllerReponseBody
{
count = item.Value,
rank = counter,
user = item.Key
});
counter++;
}
return Ok(response);
} }
catch (HighscoreListEmptyException)
{
return NotFound(new ApiError
{
Message = $"No {body.Type} found on this server"
});
}
var response = new List<HighscoreControllerReponseBody>();
var counter = 1;
foreach (var item in list)
{
response.Add(new HighscoreControllerReponseBody
{
count = item.Value,
rank = counter,
user = item.Key
});
counter++;
}
return Ok(response);
} }
} }

View file

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Geekbot.Core.Highscores;
namespace Geekbot.Web.Controllers.Highscores;
public record HighscoreControllerPostBody
{
[Required]
public ulong GuildId { get; set; }
public HighscoreTypes Type { get; } = HighscoreTypes.messages;
[Range(1, 150)]
public int Amount { get; } = 50;
}

View file

@ -1,16 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Geekbot.Core.Highscores;
namespace Geekbot.Web.Controllers.Highscores
{
public class HighscoreControllerPostBodyDto
{
[Required]
public ulong GuildId { get; set; }
public HighscoreTypes Type { get; } = HighscoreTypes.messages;
[Range(1, 150)]
public int Amount { get; } = 50;
}
}

View file

@ -1,11 +1,12 @@
using Geekbot.Core.Highscores; using Geekbot.Core.Highscores;
namespace Geekbot.Web.Controllers.Highscores namespace Geekbot.Web.Controllers.Highscores;
public record HighscoreControllerReponseBody
{ {
public class HighscoreControllerReponseBody public int rank { get; set; }
{
public int rank { get; set; } public HighscoreUserDto user { get; set; }
public HighscoreUserDto user { get; set; }
public int count { get; set; } public int count { get; set; }
}
} }

View file

@ -1,101 +1,99 @@
using System;
using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Geekbot.Core.GlobalSettings; using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Interactions; using Geekbot.Core.Interactions;
using Geekbot.Core.Interactions.Request; using Geekbot.Core.Interactions.Request;
using Geekbot.Core.Interactions.Response; using Geekbot.Core.Interactions.Response;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sodium; using Sodium;
namespace Geekbot.Web.Controllers.Interactions namespace Geekbot.Web.Controllers.Interactions;
[ApiController]
public class InteractionController : ControllerBase
{ {
public class InteractionController : Controller private readonly IInteractionCommandManager _interactionCommandManager;
private readonly byte[] _publicKeyBytes;
public InteractionController(IGlobalSettings globalSettings, IInteractionCommandManager interactionCommandManager)
{ {
private readonly IInteractionCommandManager _interactionCommandManager; _interactionCommandManager = interactionCommandManager;
private readonly byte[] _publicKeyBytes; var publicKey = globalSettings.GetKey("DiscordPublicKey");
_publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
}
public InteractionController(IGlobalSettings globalSettings, IInteractionCommandManager interactionCommandManager) [HttpPost]
[Route("/interactions")]
public async Task<IActionResult> HandleInteraction(
[FromHeader(Name = "X-Signature-Ed25519")]
string signature,
[FromHeader(Name = "X-Signature-Timestamp")]
string timestamp
)
{
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(timestamp))
{ {
_interactionCommandManager = interactionCommandManager; return BadRequest();
var publicKey = globalSettings.GetKey("DiscordPublicKey");
_publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
} }
[HttpPost] Request.EnableBuffering();
[Route("/interactions")] if (!(await HasValidSignature(signature, timestamp)))
public async Task<IActionResult> HandleInteraction(
[FromHeader(Name = "X-Signature-Ed25519")] string signature,
[FromHeader(Name = "X-Signature-Timestamp")] string timestamp
)
{ {
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(timestamp)) return Unauthorized();
{
return BadRequest();
}
Request.EnableBuffering();
if (!(await HasValidSignature(signature, timestamp)))
{
return Unauthorized();
}
if (Request.Body.CanSeek) Request.Body.Seek(0, SeekOrigin.Begin);
var interaction = await JsonSerializer.DeserializeAsync<Interaction>(Request.Body, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }).ConfigureAwait(false);
if (interaction is null) throw new JsonException("Failed to deserialize JSON body");
return (interaction.Type, interaction.Version) switch
{
(InteractionType.Ping, 1) => Ping(),
(InteractionType.ApplicationCommand, 1) => await ApplicationCommand(interaction),
(InteractionType.MessageComponent, 1) => MessageComponent(interaction),
_ => StatusCode(501)
};
} }
private IActionResult Ping() if (Request.Body.CanSeek) Request.Body.Seek(0, SeekOrigin.Begin);
var interaction = await JsonSerializer.DeserializeAsync<Interaction>(Request.Body, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }).ConfigureAwait(false);
if (interaction is null) throw new JsonException("Failed to deserialize JSON body");
return (interaction.Type, interaction.Version) switch
{ {
var response = new InteractionResponse() (InteractionType.Ping, 1) => Ping(),
{ (InteractionType.ApplicationCommand, 1) => await ApplicationCommand(interaction),
Type = InteractionResponseType.Pong, (InteractionType.MessageComponent, 1) => MessageComponent(interaction),
}; _ => StatusCode(501)
return Ok(response); };
} }
private async Task<IActionResult> ApplicationCommand(Interaction interaction) private IActionResult Ping()
{
var response = new InteractionResponse()
{ {
var result = await _interactionCommandManager.RunCommand(interaction); Type = InteractionResponseType.Pong,
};
return Ok(response);
}
if (result == null) private async Task<IActionResult> ApplicationCommand(Interaction interaction)
{ {
return StatusCode(501); var result = await _interactionCommandManager.RunCommand(interaction);
}
return Ok(result); if (result == null)
}
private IActionResult MessageComponent(Interaction interaction)
{ {
return StatusCode(501); return StatusCode(501);
} }
private async Task<bool> HasValidSignature(string signature, string timestamp) return Ok(result);
{ }
var timestampBytes = Encoding.Default.GetBytes(timestamp);
var signatureBytes = Convert.FromHexString(signature.AsSpan());
var memoryStream = new MemoryStream(); private IActionResult MessageComponent(Interaction interaction)
await Request.Body.CopyToAsync(memoryStream).ConfigureAwait(false); {
var body = memoryStream.ToArray(); return StatusCode(501);
}
var timestampLength = timestampBytes.Length; private async Task<bool> HasValidSignature(string signature, string timestamp)
Array.Resize(ref timestampBytes, timestampLength + body.Length); {
Array.Copy(body, 0, timestampBytes, timestampLength, body.Length); var timestampBytes = Encoding.Default.GetBytes(timestamp);
var signatureBytes = Convert.FromHexString(signature.AsSpan());
return PublicKeyAuth.VerifyDetached(signatureBytes, timestampBytes, _publicKeyBytes); var memoryStream = new MemoryStream();
} await Request.Body.CopyToAsync(memoryStream).ConfigureAwait(false);
var body = memoryStream.ToArray();
var timestampLength = timestampBytes.Length;
Array.Resize(ref timestampBytes, timestampLength + body.Length);
Array.Copy(body, 0, timestampBytes, timestampLength, body.Length);
return PublicKeyAuth.VerifyDetached(signatureBytes, timestampBytes, _publicKeyBytes);
} }
} }

View file

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks;
using Geekbot.Core; using Geekbot.Core;
using Geekbot.Core.GlobalSettings; using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Interactions; using Geekbot.Core.Interactions;
@ -11,129 +6,128 @@ using Geekbot.Core.Interactions.ApplicationCommand;
using Geekbot.Core.Logger; using Geekbot.Core.Logger;
using Geekbot.Web.Controllers.Interactions.Model; using Geekbot.Web.Controllers.Interactions.Model;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sentry.Protocol;
namespace Geekbot.Web.Controllers.Interactions namespace Geekbot.Web.Controllers.Interactions;
[ApiController]
public class InteractionRegistrarController : ControllerBase
{ {
public class InteractionRegistrarController : Controller private readonly IGeekbotLogger _logger;
private readonly IInteractionCommandManager _interactionCommandManager;
private readonly string _discordToken;
private readonly Uri _guildCommandUri;
public InteractionRegistrarController(IGlobalSettings globalSettings, IGeekbotLogger logger, IInteractionCommandManager interactionCommandManager)
{ {
private readonly IGeekbotLogger _logger; _logger = logger;
private readonly IInteractionCommandManager _interactionCommandManager; _interactionCommandManager = interactionCommandManager;
private readonly string _discordToken; _discordToken = globalSettings.GetKey("DiscordToken");
private readonly Uri _guildCommandUri; var applicationId = globalSettings.GetKey("DiscordApplicationId");
var betaGuilds = new Dictionary<string, string>()
public InteractionRegistrarController(IGlobalSettings globalSettings, IGeekbotLogger logger, IInteractionCommandManager interactionCommandManager)
{ {
_logger = logger; { "171249478546882561", "93070552863870976" }, // Prod / Swiss Geeks
_interactionCommandManager = interactionCommandManager; { "181092911243329537", "131827972083548160" }, // Dev / Rune's Playground
_discordToken = globalSettings.GetKey("DiscordToken"); };
var applicationId = globalSettings.GetKey("DiscordApplicationId"); _guildCommandUri = new Uri($"https://discord.com/api/v8/applications/{applicationId}/guilds/{betaGuilds[applicationId]}/commands");
var betaGuilds = new Dictionary<string, string>() }
{
{ "171249478546882561", "93070552863870976" }, // Prod / Swiss Geeks
{ "181092911243329537", "131827972083548160" }, // Dev / Rune's Playground
};
_guildCommandUri = new Uri($"https://discord.com/api/v8/applications/{applicationId}/guilds/{betaGuilds[applicationId]}/commands");
}
[HttpPost]
[Route("/interactions/register")]
public async Task<IActionResult> RegisterInteractions()
{
var registeredInteractions = await GetRegisteredInteractions();
var operations = new InteractionRegistrationOperations();
foreach (var (_, command) in _interactionCommandManager.CommandsInfo) [HttpPost]
[Route("/interactions/register")]
public async Task<IActionResult> RegisterInteractions()
{
var registeredInteractions = await GetRegisteredInteractions();
var operations = new InteractionRegistrationOperations();
foreach (var (_, command) in _interactionCommandManager.CommandsInfo)
{
var existing = registeredInteractions.FirstOrDefault(i => i.Name == command.Name);
if (existing == null)
{ {
var existing = registeredInteractions.FirstOrDefault(i => i.Name == command.Name); operations.Create.Add(command);
if (existing == null)
{
operations.Create.Add(command);
}
else
{
operations.Update.Add(existing.Id, command);
}
} }
else
foreach (var registeredInteraction in registeredInteractions.Where(registeredInteraction => !_interactionCommandManager.CommandsInfo.Values.Any(c => c.Name == registeredInteraction.Name)))
{ {
operations.Remove.Add(registeredInteraction.Id); operations.Update.Add(existing.Id, command);
}
await Task.WhenAll(new[]
{
Create(operations.Create),
Update(operations.Update),
Remove(operations.Remove)
});
return Ok(operations);
}
private HttpClient CreateClientWithToken()
{
var httpClient = HttpAbstractions.CreateDefaultClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bot", _discordToken);
return httpClient;
}
private async Task Create(List<Command> commands)
{
foreach (var command in commands)
{
try
{
await HttpAbstractions.Post(_guildCommandUri, command, CreateClientWithToken());
_logger.Information(LogSource.Interaction, $"Registered interaction: {command.Name}");
}
catch (Exception e)
{
_logger.Error(LogSource.Interaction, $"Failed to register interaction: {command.Name}", e);
}
} }
} }
private async Task Update(Dictionary<string, Command> commands) foreach (var registeredInteraction in registeredInteractions.Where(registeredInteraction => !_interactionCommandManager.CommandsInfo.Values.Any(c => c.Name == registeredInteraction.Name)))
{ {
foreach (var (id, command) in commands) operations.Remove.Add(registeredInteraction.Id);
{
try
{
var updateUri = new Uri(_guildCommandUri.AbsoluteUri + $"/{id}");
await HttpAbstractions.Patch(updateUri, command, CreateClientWithToken());
_logger.Information(LogSource.Interaction, $"Updated Interaction: {command.Name}");
}
catch (Exception e)
{
_logger.Error(LogSource.Interaction, $"Failed to update Interaction: {command.Name}", e);
}
}
} }
private async Task Remove(List<string> commands) await Task.WhenAll(new[]
{ {
foreach (var id in commands) Create(operations.Create),
Update(operations.Update),
Remove(operations.Remove)
});
return Ok(operations);
}
private HttpClient CreateClientWithToken()
{
var httpClient = HttpAbstractions.CreateDefaultClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bot", _discordToken);
return httpClient;
}
private async Task Create(List<Command> commands)
{
foreach (var command in commands)
{
try
{ {
try await HttpAbstractions.Post(_guildCommandUri, command, CreateClientWithToken());
{ _logger.Information(LogSource.Interaction, $"Registered interaction: {command.Name}");
var updateUri = new Uri(_guildCommandUri.AbsoluteUri + $"/{id}"); }
await HttpAbstractions.Delete(updateUri, CreateClientWithToken()); catch (Exception e)
_logger.Information(LogSource.Interaction, $"Deleted interaction with ID: {id}"); {
} _logger.Error(LogSource.Interaction, $"Failed to register interaction: {command.Name}", e);
catch (Exception e)
{
_logger.Error(LogSource.Interaction, $"Failed to delete interaction with id: {id}", e);
}
} }
}
private async Task<List<RegisteredInteraction>> GetRegisteredInteractions()
{
var httpClient = HttpAbstractions.CreateDefaultClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bot", _discordToken);
return await HttpAbstractions.Get<List<RegisteredInteraction>>(_guildCommandUri, httpClient);
} }
} }
private async Task Update(Dictionary<string, Command> commands)
{
foreach (var (id, command) in commands)
{
try
{
var updateUri = new Uri(_guildCommandUri.AbsoluteUri + $"/{id}");
await HttpAbstractions.Patch(updateUri, command, CreateClientWithToken());
_logger.Information(LogSource.Interaction, $"Updated Interaction: {command.Name}");
}
catch (Exception e)
{
_logger.Error(LogSource.Interaction, $"Failed to update Interaction: {command.Name}", e);
}
}
}
private async Task Remove(List<string> commands)
{
foreach (var id in commands)
{
try
{
var updateUri = new Uri(_guildCommandUri.AbsoluteUri + $"/{id}");
await HttpAbstractions.Delete(updateUri, CreateClientWithToken());
_logger.Information(LogSource.Interaction, $"Deleted interaction with ID: {id}");
}
catch (Exception e)
{
_logger.Error(LogSource.Interaction, $"Failed to delete interaction with id: {id}", e);
}
}
}
private async Task<List<RegisteredInteraction>> GetRegisteredInteractions()
{
var httpClient = HttpAbstractions.CreateDefaultClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bot", _discordToken);
return await HttpAbstractions.Get<List<RegisteredInteraction>>(_guildCommandUri, httpClient);
}
} }

View file

@ -1,12 +1,12 @@
using System.Collections.Generic;
using Geekbot.Core.Interactions.ApplicationCommand; using Geekbot.Core.Interactions.ApplicationCommand;
namespace Geekbot.Web.Controllers.Interactions.Model namespace Geekbot.Web.Controllers.Interactions.Model;
public record InteractionRegistrationOperations
{ {
public record InteractionRegistrationOperations public List<Command> Create { get; set; } = new();
{
public List<Command> Create { get; set; } = new(); public Dictionary<string, Command> Update { get; set; } = new();
public Dictionary<string, Command> Update { get; set; } = new();
public List<string> Remove { get; set; } = new(); public List<string> Remove { get; set; } = new();
}
} }

View file

@ -1,32 +1,31 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Geekbot.Core.Interactions.Request; using Geekbot.Core.Interactions.Request;
namespace Geekbot.Web.Controllers.Interactions.Model namespace Geekbot.Web.Controllers.Interactions.Model;
public record RegisteredInteraction
{ {
public record RegisteredInteraction [JsonPropertyName("id")]
{ public string Id { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; } [JsonPropertyName("application_id")]
public string ApplicationId { get; set; }
[JsonPropertyName("application_id")]
public string ApplicationId { get; set; } [JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } [JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; } [JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; } [JsonPropertyName("default_permission")]
public bool DefaultPermission { get; set; }
[JsonPropertyName("default_permission")]
public bool DefaultPermission { get; set; } [JsonPropertyName("type")]
public InteractionType Type { get; set; }
[JsonPropertyName("type")]
public InteractionType Type { get; set; } [JsonPropertyName("guild_id")]
public string GuildId { get; set; }
[JsonPropertyName("guild_id")]
public string GuildId { get; set; }
}
} }

View file

@ -1,64 +1,63 @@
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Geekbot.Core; using Geekbot.Core;
using Geekbot.Core.GlobalSettings; using Geekbot.Core.GlobalSettings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Geekbot.Web.Controllers namespace Geekbot.Web.Controllers;
/*
* Sometimes for some unknown reason command responses may become incredibly slow.
* Because i don't have time to debug it and may not always know directly when it happens,
* i want to give some trusted people the possibility to restart the bot.
* The kill code must be set manually in the db, it should end it `---1'
*
* ToDo: Actually fix the underlying issue
*/
[ApiController]
public class KillController : ControllerBase
{ {
/* private readonly IGlobalSettings _globalSettings;
* Sometimes for some unknown reason command responses may become incredibly slow. private const string KillCodeDbKey = "KillCode";
* Because i don't have time to debug it and may not always know directly when it happens,
* i want to give some trusted people the possibility to restart the bot. public KillController(IGlobalSettings globalSettings)
* The kill code must be set manually in the db, it should end it `---1'
*
* ToDo: Actually fix the underlying issue
*/
public class KillController : Controller
{ {
private readonly IGlobalSettings _globalSettings; _globalSettings = globalSettings;
private const string KillCodeDbKey = "KillCode"; }
public KillController(IGlobalSettings globalSettings) [HttpGet("/v1/kill/{providedKillCode}")]
public async Task<IActionResult> KillTheBot([FromRoute] string providedKillCode)
{
var killCode = _globalSettings.GetKey(KillCodeDbKey);
if (providedKillCode != killCode)
{ {
_globalSettings = globalSettings; return Unauthorized(new ApiError { Message = $"Go Away ({GetKillCodeInt(killCode)})" });
} }
[HttpGet("/v1/kill/{providedKillCode}")] await UpdateKillCode(killCode);
public async Task<IActionResult> KillTheBot([FromRoute] string providedKillCode)
{
var killCode = _globalSettings.GetKey(KillCodeDbKey);
if (providedKillCode != killCode)
{
return Unauthorized(new ApiError { Message = $"Go Away ({GetKillCodeInt(killCode)})" });
}
await UpdateKillCode(killCode);
#pragma warning disable CS4014 #pragma warning disable CS4014
Kill(); Kill();
#pragma warning restore CS4014 #pragma warning restore CS4014
return Ok(); return Ok();
} }
private int GetKillCodeInt(string code) private int GetKillCodeInt(string code)
{ {
var group = Regex.Match(code, @".+(\-\-\-(?<int>\d+))").Groups["int"]; var group = Regex.Match(code, @".+(\-\-\-(?<int>\d+))").Groups["int"];
return group.Success ? int.Parse(group.Value) : 0; return group.Success ? int.Parse(group.Value) : 0;
} }
private async Task UpdateKillCode(string oldCode)
{
var newKillCodeInt = new Random().Next(1, 100);
var newCode = oldCode.Replace($"---{GetKillCodeInt(oldCode)}", $"---{newKillCodeInt}");
await _globalSettings.SetKey(KillCodeDbKey, newCode);
}
private async Task Kill() private async Task UpdateKillCode(string oldCode)
{ {
// wait a second so the http response can still be sent successfully var newKillCodeInt = new Random().Next(1, 100);
await Task.Delay(1000); var newCode = oldCode.Replace($"---{GetKillCodeInt(oldCode)}", $"---{newKillCodeInt}");
Environment.Exit(GeekbotExitCode.KilledByApiCall.GetHashCode()); await _globalSettings.SetKey(KillCodeDbKey, newCode);
} }
private async Task Kill()
{
// wait a second so the http response can still be sent successfully
await Task.Delay(1000);
Environment.Exit(GeekbotExitCode.KilledByApiCall.GetHashCode());
} }
} }

View file

@ -0,0 +1,10 @@
namespace Geekbot.Web.Controllers.Status;
public record ApiStatus
{
public string GeekbotVersion { get; set; }
public string ApiVersion { get; set; }
public string Status { get; set; }
}

View file

@ -1,9 +0,0 @@
namespace Geekbot.Web.Controllers.Status
{
public class ApiStatusDto
{
public string GeekbotVersion { get; set; }
public string ApiVersion { get; set; }
public string Status { get; set; }
}
}

View file

@ -3,21 +3,21 @@ using Geekbot.Core;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Geekbot.Web.Controllers.Status namespace Geekbot.Web.Controllers.Status;
[ApiController]
[EnableCors("AllowSpecificOrigin")]
public class StatusController : ControllerBase
{ {
[EnableCors("AllowSpecificOrigin")] [Route("/")]
public class StatusController : Controller public IActionResult GetCommands()
{ {
[Route("/")] var responseBody = new ApiStatus
public IActionResult GetCommands()
{ {
var responseBody = new ApiStatusDto GeekbotVersion = Constants.BotVersion(),
{ ApiVersion = Constants.ApiVersion.ToString(CultureInfo.InvariantCulture),
GeekbotVersion = Constants.BotVersion(), Status = "Online"
ApiVersion = Constants.ApiVersion.ToString(CultureInfo.InvariantCulture), };
Status = "Online" return Ok(responseBody);
};
return Ok(responseBody);
}
} }
} }