Creat initial interaction command framework
This commit is contained in:
parent
60547140ea
commit
65bb7f6cac
11 changed files with 409 additions and 15 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/Core/Interactions/IInteractionBase.cs
Normal file
9
src/Core/Interactions/IInteractionBase.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Geekbot.Core.Interactions
|
||||
{
|
||||
public interface IInteractionBase
|
||||
{
|
||||
void BeforeExecute();
|
||||
void AfterExecute();
|
||||
void OnException();
|
||||
}
|
||||
}
|
14
src/Core/Interactions/IInteractionCommandManager.cs
Normal file
14
src/Core/Interactions/IInteractionCommandManager.cs
Normal 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);
|
||||
}
|
||||
}
|
31
src/Core/Interactions/InteractionBase.cs
Normal file
31
src/Core/Interactions/InteractionBase.cs
Normal 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();
|
||||
}
|
||||
}
|
47
src/Core/Interactions/InteractionCommandManager.cs
Normal file
47
src/Core/Interactions/InteractionCommandManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ namespace Geekbot.Core.Logger
|
|||
Api,
|
||||
Migration,
|
||||
HighscoreManager,
|
||||
Interaction,
|
||||
Other
|
||||
}
|
||||
}
|
|
@ -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,14 +15,16 @@ namespace Geekbot.Web.Controllers.Interactions
|
|||
{
|
||||
public class InteractionController : Controller
|
||||
{
|
||||
private readonly byte[] publicKeyBytes;
|
||||
|
||||
public InteractionController(IGlobalSettings globalSettings)
|
||||
private readonly IInteractionCommandManager _interactionCommandManager;
|
||||
private readonly byte[] _publicKeyBytes;
|
||||
|
||||
public InteractionController(IGlobalSettings globalSettings, IInteractionCommandManager interactionCommandManager)
|
||||
{
|
||||
_interactionCommandManager = interactionCommandManager;
|
||||
var publicKey = globalSettings.GetKey("DiscordPublicKey");
|
||||
publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
|
||||
_publicKeyBytes = Convert.FromHexString(publicKey.AsSpan());
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("/interactions")]
|
||||
public async Task<IActionResult> HandleInteraction(
|
||||
|
@ -32,7 +36,7 @@ namespace Geekbot.Web.Controllers.Interactions
|
|||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
|
||||
Request.EnableBuffering();
|
||||
if (!(await HasValidSignature(signature, timestamp)))
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -75,7 +86,7 @@ namespace Geekbot.Web.Controllers.Interactions
|
|||
{
|
||||
var timestampBytes = Encoding.Default.GetBytes(timestamp);
|
||||
var signatureBytes = Convert.FromHexString(signature.AsSpan());
|
||||
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
await Request.Body.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
var body = memoryStream.ToArray();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -41,10 +42,14 @@ namespace Geekbot.Web
|
|||
builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
|
||||
});
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue