Creat initial interaction command framework

This commit is contained in:
Daan Boerlage 2021-09-20 01:31:24 +02:00
parent 60547140ea
commit 65bb7f6cac
Signed by: daan
GPG key ID: FCE070E1E4956606
11 changed files with 409 additions and 15 deletions

View file

@ -1,6 +1,9 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Newtonsoft.Json;
@ -22,7 +25,7 @@ namespace Geekbot.Core
return client;
}
public static async Task<T> Get<T>(Uri location, HttpClient httpClient = null, bool disposeClient = true)
public static async Task<TResponse> Get<TResponse>(Uri location, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
@ -36,7 +39,83 @@ namespace Geekbot.Core
httpClient.Dispose();
}
return JsonConvert.DeserializeObject<T>(stringResponse);
return JsonConvert.DeserializeObject<TResponse>(stringResponse);
}
public static async Task<TResponse> Post<TResponse>(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),
Encoding.UTF8,
"application/json"
);
var response = await httpClient.PostAsync(location.PathAndQuery, content);
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
if (disposeClient)
{
httpClient.Dispose();
}
return JsonConvert.DeserializeObject<TResponse>(stringResponse);
}
public static async Task Post(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),
Encoding.UTF8,
"application/json"
);
var response = await httpClient.PostAsync(location, content);
response.EnsureSuccessStatusCode();
if (disposeClient)
{
httpClient.Dispose();
}
}
public static async Task Patch(Uri location, object data, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(data, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),
Encoding.UTF8,
"application/json"
);
var response = await httpClient.PatchAsync(location, content);
response.EnsureSuccessStatusCode();
if (disposeClient)
{
httpClient.Dispose();
}
}
public static async Task Delete(Uri location, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
var response = await httpClient.DeleteAsync(location);
response.EnsureSuccessStatusCode();
if (disposeClient)
{
httpClient.Dispose();
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.Core.Interactions
{
public interface IInteractionBase
{
void BeforeExecute();
void AfterExecute();
void OnException();
}
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Geekbot.Core.Interactions.ApplicationCommand;
using Geekbot.Core.Interactions.Request;
using Geekbot.Core.Interactions.Response;
namespace Geekbot.Core.Interactions
{
public interface IInteractionCommandManager
{
Dictionary<string, Command> CommandsInfo { get; init; }
Task<InteractionResponse> RunCommand(Interaction interaction);
}
}

View file

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using Geekbot.Core.Interactions.ApplicationCommand;
using Geekbot.Core.Interactions.Request;
using Geekbot.Core.Interactions.Response;
namespace Geekbot.Core.Interactions
{
public abstract class InteractionBase : IInteractionBase
{
protected virtual void BeforeExecute()
{
}
protected virtual void AfterExecute()
{
}
protected virtual void OnException()
{
}
public abstract Command GetCommandInfo();
public abstract Task<InteractionResponse> Exec(InteractionData interaction);
void IInteractionBase.BeforeExecute() => this.BeforeExecute();
void IInteractionBase.AfterExecute() => this.AfterExecute();
void IInteractionBase.OnException() => this.OnException();
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Interactions.ApplicationCommand;
using Geekbot.Core.Interactions.Request;
using Geekbot.Core.Interactions.Response;
using Geekbot.Core.Logger;
namespace Geekbot.Core.Interactions
{
public class InteractionCommandManager : IInteractionCommandManager
{
private readonly Dictionary<string, Type> _commands = new();
public Dictionary<string, Command> CommandsInfo { get; init; }
public InteractionCommandManager()
{
var interactions = Assembly.GetCallingAssembly()
.GetTypes()
.Where(type => type.IsClass && !type.IsAbstract && type.IsSubclassOf(typeof(InteractionBase)))
.ToList();
CommandsInfo = new Dictionary<string, Command>();
foreach (var interactionType in interactions)
{
var instance = (InteractionBase)Activator.CreateInstance(interactionType);
var commandInfo = instance.GetCommandInfo();
_commands.Add(commandInfo.Name, interactionType);
CommandsInfo.Add(commandInfo.Name, commandInfo);
}
}
public async Task<InteractionResponse> RunCommand(Interaction interaction)
{
var type = _commands[interaction.Data.Name];
var command = (InteractionBase)Activator.CreateInstance(type);
return await command.Exec(interaction.Data);
}
}
}

View file

@ -17,6 +17,7 @@ namespace Geekbot.Core.Logger
Api,
Migration,
HighscoreManager,
Interaction,
Other
}
}

View file

@ -4,7 +4,9 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Geekbot.Core.GlobalSettings;
using Geekbot.Web.Controllers.Interactions.Model;
using Geekbot.Core.Interactions;
using Geekbot.Core.Interactions.Request;
using Geekbot.Core.Interactions.Response;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Sodium;
@ -13,12 +15,14 @@ namespace Geekbot.Web.Controllers.Interactions
{
public class InteractionController : Controller
{
private readonly byte[] publicKeyBytes;
private readonly IInteractionCommandManager _interactionCommandManager;
private readonly byte[] _publicKeyBytes;
public InteractionController(IGlobalSettings globalSettings)
public InteractionController(IGlobalSettings globalSettings, IInteractionCommandManager interactionCommandManager)
{
_interactionCommandManager = interactionCommandManager;
var publicKey = globalSettings.GetKey("DiscordPublicKey");
publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
_publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
}
[HttpPost]
@ -46,7 +50,7 @@ namespace Geekbot.Web.Controllers.Interactions
return (interaction.Type, interaction.Version) switch
{
(InteractionType.Ping, 1) => Ping(),
(InteractionType.ApplicationCommand, 1) => ApplicationCommand(interaction),
(InteractionType.ApplicationCommand, 1) => await ApplicationCommand(interaction),
(InteractionType.MessageComponent, 1) => MessageComponent(interaction),
_ => StatusCode(501)
};
@ -61,9 +65,16 @@ namespace Geekbot.Web.Controllers.Interactions
return Ok(response);
}
private IActionResult ApplicationCommand(Interaction interaction)
private async Task<IActionResult> ApplicationCommand(Interaction interaction)
{
return StatusCode(501);
var result = await _interactionCommandManager.RunCommand(interaction);
if (result == null)
{
return StatusCode(501);
}
return Ok(result);
}
private IActionResult MessageComponent(Interaction interaction)
@ -84,7 +95,7 @@ namespace Geekbot.Web.Controllers.Interactions
Array.Resize(ref timestampBytes, timestampLength + body.Length);
Array.Copy(body, 0, timestampBytes, timestampLength, body.Length);
return PublicKeyAuth.VerifyDetached(signatureBytes, timestampBytes, publicKeyBytes);
return PublicKeyAuth.VerifyDetached(signatureBytes, timestampBytes, _publicKeyBytes);
}
}
}

View file

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Geekbot.Core;
using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Interactions;
using Geekbot.Core.Interactions.ApplicationCommand;
using Geekbot.Core.Logger;
using Geekbot.Web.Controllers.Interactions.Model;
using Microsoft.AspNetCore.Mvc;
using Sentry.Protocol;
namespace Geekbot.Web.Controllers.Interactions
{
public class InteractionRegistrarController : Controller
{
private readonly IGeekbotLogger _logger;
private readonly IInteractionCommandManager _interactionCommandManager;
private readonly string _discordToken;
// private readonly string _applicationId;
private readonly Uri _guildCommandUri;
public InteractionRegistrarController(IGlobalSettings globalSettings, IGeekbotLogger logger, IInteractionCommandManager interactionCommandManager)
{
_logger = logger;
_interactionCommandManager = interactionCommandManager;
_discordToken = globalSettings.GetKey("DiscordToken");
var applicationId = globalSettings.GetKey("DiscordApplicationId");
var runesPlayground = "131827972083548160";
_guildCommandUri = new Uri($"https://discord.com/api/v8/applications/{applicationId}/guilds/{runesPlayground}/commands");
}
[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)
{
operations.Create.Add(command);
}
else
{
operations.Update.Add(existing.Id, command);
}
}
foreach (var registeredInteraction in registeredInteractions.Where(registeredInteraction => !_interactionCommandManager.CommandsInfo.Values.Any(c => c.Name == registeredInteraction.Name)))
{
operations.Remove.Add(registeredInteraction.Id);
}
// foreach (var (_, command) in _interactionCommandManager.CommandsInfo)
// {
// try
// {
// var httpClient = HttpAbstractions.CreateDefaultClient();
// httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bot", _discordToken);
//
// await HttpAbstractions.Post(_guildCommandUri, command, httpClient);
//
// _logger.Information(LogSource.Interaction, $"Registered Interaction: {command.Name}");
// }
// catch (Exception e)
// {
// _logger.Error(LogSource.Interaction, $"Failed to register Interaction: {command.Name}", e);
// }
// }
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 (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

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

View file

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

View file

@ -6,6 +6,7 @@ using Geekbot.Core;
using Geekbot.Core.Database;
using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Highscores;
using Geekbot.Core.Interactions;
using Geekbot.Core.Logger;
using Geekbot.Web.Logging;
using Microsoft.AspNetCore;
@ -42,9 +43,13 @@ namespace Geekbot.Web
});
services.AddSentry();
var interactionCommandManager = new InteractionCommandManager();
services.AddSingleton(databaseContext);
services.AddSingleton(globalSettings);
services.AddSingleton(highscoreManager);
services.AddSingleton(logger);
services.AddSingleton<IInteractionCommandManager>(interactionCommandManager);
if (runParameters.DisableGateway) return;
services.AddSingleton(commandService);