From d81fb2a3d977786079a69525f1d17357fb520ea0 Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Sun, 19 Sep 2021 16:11:06 +0200 Subject: [PATCH] Add initial interaction support --- .../Interactions/InteractionController.cs | 90 +++++++++++++++++++ .../Model/ApplicationCommandOption.cs | 16 ++++ .../Interactions/Model/Interaction.cs | 22 +++++ .../Interactions/Model/InteractionData.cs | 14 +++ .../Interactions/Model/InteractionOption.cs | 12 +++ .../Model/InteractionResolvedData.cs | 15 ++++ .../Interactions/Model/InteractionResponse.cs | 13 +++ .../Model/InteractionResponseData.cs | 16 ++++ .../Model/InteractionResponseType.cs | 11 +++ .../Interactions/Model/InteractionType.cs | 9 ++ .../Model/MessageComponents/Component.cs | 7 ++ .../Interactions/Model/Resolved/Channel.cs | 12 +++ .../Model/Resolved/ChannelType.cs | 17 ++++ .../Interactions/Model/Resolved/Member.cs | 16 ++++ .../Interactions/Model/Resolved/RoleTag.cs | 9 ++ .../Interactions/Model/Resolved/Roles.cs | 15 ++++ .../Model/Resolved/ThreadMetadata.cs | 13 +++ .../Interactions/Model/Resolved/User.cs | 21 +++++ src/Web/Web.csproj | 4 + src/Web/WebApiStartup.cs | 5 +- 20 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 src/Web/Controllers/Interactions/InteractionController.cs create mode 100644 src/Web/Controllers/Interactions/Model/ApplicationCommandOption.cs create mode 100644 src/Web/Controllers/Interactions/Model/Interaction.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionData.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionOption.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionResolvedData.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionResponse.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionResponseData.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionResponseType.cs create mode 100644 src/Web/Controllers/Interactions/Model/InteractionType.cs create mode 100644 src/Web/Controllers/Interactions/Model/MessageComponents/Component.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/Channel.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/ChannelType.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/Member.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/RoleTag.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/Roles.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/ThreadMetadata.cs create mode 100644 src/Web/Controllers/Interactions/Model/Resolved/User.cs diff --git a/src/Web/Controllers/Interactions/InteractionController.cs b/src/Web/Controllers/Interactions/InteractionController.cs new file mode 100644 index 0000000..3501c06 --- /dev/null +++ b/src/Web/Controllers/Interactions/InteractionController.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Geekbot.Core.GlobalSettings; +using Geekbot.Web.Controllers.Interactions.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sodium; + +namespace Geekbot.Web.Controllers.Interactions +{ + public class InteractionController : Controller + { + private readonly byte[] publicKeyBytes; + + public InteractionController(IGlobalSettings globalSettings) + { + var publicKey = globalSettings.GetKey("DiscordPublicKey"); + publicKeyBytes = Convert.FromHexString(publicKey.AsSpan()); + } + + [HttpPost] + [Route("/interactions")] + public async Task HandleInteraction( + [FromHeader(Name = "X-Signature-Ed25519")] string signature, + [FromHeader(Name = "X-Signature-Timestamp")] string timestamp + ) + { + if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(timestamp)) + { + 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(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) => ApplicationCommand(interaction), + (InteractionType.MessageComponent, 1) => MessageComponent(interaction), + _ => StatusCode(501) + }; + } + + private IActionResult Ping() + { + var response = new InteractionResponse() + { + Type = InteractionResponseType.Pong, + }; + return Ok(response); + } + + private IActionResult ApplicationCommand(Interaction interaction) + { + return StatusCode(501); + } + + private IActionResult MessageComponent(Interaction interaction) + { + return StatusCode(501); + } + + private async Task HasValidSignature(string signature, string timestamp) + { + 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(); + + 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); + } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/ApplicationCommandOption.cs b/src/Web/Controllers/Interactions/Model/ApplicationCommandOption.cs new file mode 100644 index 0000000..794102b --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/ApplicationCommandOption.cs @@ -0,0 +1,16 @@ +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public enum ApplicationCommandOption + { + SubCommand = 1, + SubCommandGroup = 2, + String = 3, + Integer = 4, + Boolean = 5, + User = 6, + Channel = 7, + Role = 8, + Mentionable = 9, + Number = 10, + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Interaction.cs b/src/Web/Controllers/Interactions/Model/Interaction.cs new file mode 100644 index 0000000..6045504 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Interaction.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public record Interaction + { + public string Id { get; set; } + public string ApplicationId { get; init; } + [Required] + public InteractionType Type { get; set; } + public InteractionData Data { get; init; } + public string GuildId { get; init; } + public string Name { get; init; } + public string Description { get; init; } + public List Options { get; init; } + public bool DefaultPermission { get; init; } + [Required] + public int Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionData.cs b/src/Web/Controllers/Interactions/Model/InteractionData.cs new file mode 100644 index 0000000..cb0f594 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionData.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public record InteractionData + { + public string Id { get; set; } + public string Name { get; set; } + public int Type { get; set;} + public InteractionResolvedData Resolved { get; set; } + public List Options { get; set; } + public string TargetId { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionOption.cs b/src/Web/Controllers/Interactions/Model/InteractionOption.cs new file mode 100644 index 0000000..c5acfa7 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionOption.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public record InteractionOption + { + public string Name { get; set; } + public ApplicationCommandOption Type { get; set; } + public string Value { get; set; } + public List Options { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionResolvedData.cs b/src/Web/Controllers/Interactions/Model/InteractionResolvedData.cs new file mode 100644 index 0000000..ee90f48 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionResolvedData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Discord; +using Geekbot.Web.Controllers.Interactions.Model.Resolved; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public class InteractionResolvedData + { + public Dictionary Users { get; set; } + public Dictionary Members { get; set; } + public Dictionary Roles { get; set; } + public Dictionary Channels { get; set; } + // public Dictionary Messages { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionResponse.cs b/src/Web/Controllers/Interactions/Model/InteractionResponse.cs new file mode 100644 index 0000000..c3a2a1e --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public record InteractionResponse + { + [JsonPropertyName("type")] + public InteractionResponseType Type { get; set; } + + [JsonPropertyName("data")] + public InteractionData Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionResponseData.cs b/src/Web/Controllers/Interactions/Model/InteractionResponseData.cs new file mode 100644 index 0000000..146e812 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionResponseData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Discord; +using Geekbot.Web.Controllers.Interactions.Model.MessageComponents; + +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public record InteractionResponseData + { + public bool Tts { get; set; } = false; + public string Content { get; set; } + public List Embeds { get; set; } + public AllowedMentions AllowedMentions { get; set; } + public int Flags { get; set; } + public List Components { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionResponseType.cs b/src/Web/Controllers/Interactions/Model/InteractionResponseType.cs new file mode 100644 index 0000000..5bc85dc --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionResponseType.cs @@ -0,0 +1,11 @@ +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public enum InteractionResponseType + { + Pong = 1, + ChannelMessageWithSource = 4, + DeferredChannelMessageWithSource = 5, + DeferredUpdateMessage = 6, + UpdateMessage = 7, + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/InteractionType.cs b/src/Web/Controllers/Interactions/Model/InteractionType.cs new file mode 100644 index 0000000..a8421c5 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/InteractionType.cs @@ -0,0 +1,9 @@ +namespace Geekbot.Web.Controllers.Interactions.Model +{ + public enum InteractionType + { + Ping = 1, + ApplicationCommand = 2, + MessageComponent = 3, + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/MessageComponents/Component.cs b/src/Web/Controllers/Interactions/Model/MessageComponents/Component.cs new file mode 100644 index 0000000..72097a4 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/MessageComponents/Component.cs @@ -0,0 +1,7 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.MessageComponents +{ + public record Component + { + + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/Channel.cs b/src/Web/Controllers/Interactions/Model/Resolved/Channel.cs new file mode 100644 index 0000000..4b280d9 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/Channel.cs @@ -0,0 +1,12 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record Channel + { + public string Id { get; set; } + public ChannelType Type { get; set; } + public string Name { get; set; } + public string ParentId { get; set; } + public ThreadMetadata ThreadMetadata { get; set; } + public string Permissions { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/ChannelType.cs b/src/Web/Controllers/Interactions/Model/Resolved/ChannelType.cs new file mode 100644 index 0000000..1fbc0af --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/ChannelType.cs @@ -0,0 +1,17 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public enum ChannelType + { + GuildText = 0, + Dm = 1, + GuildVoice = 2, + GroupDm = 3, + GuildCategory = 4, + GuildNews = 5, + GuildStore = 6, + GuildNewsThread = 10, + GuildPublicThread = 11, + GuildPrivateThread = 12, + GuildStageVoice = 13, + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/Member.cs b/src/Web/Controllers/Interactions/Model/Resolved/Member.cs new file mode 100644 index 0000000..e95a09a --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/Member.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record Member + { + // public User User { get; set; } + public string Nick { get; set; } + public List Roles { get; set; } + public DateTime JoinedAt { get; set; } + public DateTime PremiumSince { get; set; } + public bool Pending { get; set; } + public string Permissions { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/RoleTag.cs b/src/Web/Controllers/Interactions/Model/Resolved/RoleTag.cs new file mode 100644 index 0000000..4f35ae1 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/RoleTag.cs @@ -0,0 +1,9 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record RoleTag + { + public string BotId { get; set; } + public string IntegrationId { get; set; } + public bool PremiumSubscriber { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/Roles.cs b/src/Web/Controllers/Interactions/Model/Resolved/Roles.cs new file mode 100644 index 0000000..aab5d7c --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/Roles.cs @@ -0,0 +1,15 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record Roles + { + public string Id { get; set; } + public string Name { get; set; } + public int Color { get; set; } + public bool Hoist { get; set; } + public int Position { get; set; } + public string Permissions { get; set; } + public bool Managed { get; set; } + public bool Mentionable { get; set; } + public RoleTag Tags { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/ThreadMetadata.cs b/src/Web/Controllers/Interactions/Model/Resolved/ThreadMetadata.cs new file mode 100644 index 0000000..e43ad89 --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/ThreadMetadata.cs @@ -0,0 +1,13 @@ +using System; + +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record ThreadMetadata + { + public bool Archived { get; set; } + public int AutoArchiveDuration { get; set; } + public DateTime ArchiveTimestamp { get; set; } + public bool Locked { get; set; } + public bool Invitable { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Controllers/Interactions/Model/Resolved/User.cs b/src/Web/Controllers/Interactions/Model/Resolved/User.cs new file mode 100644 index 0000000..4ef1f8d --- /dev/null +++ b/src/Web/Controllers/Interactions/Model/Resolved/User.cs @@ -0,0 +1,21 @@ +namespace Geekbot.Web.Controllers.Interactions.Model.Resolved +{ + public record User + { + public string Id { get; set; } + public string Username { get; set; } + public string Discriminator { get; set; } + public string Avatar { get; set; } + public bool Bot { get; set; } + public bool System { get; set; } + public bool MfaEnabled { get; set; } + public string Banner { get; set; } + public int AccentColor { get; set; } + public string Locale { get; set; } + public bool Verified { get; set; } + public string Email { get; set; } + public int Flags { get; set; } + public int PremiumType { get; set; } + public int PublicFlags { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index a9ffcc4..dc6b09a 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/Web/WebApiStartup.cs b/src/Web/WebApiStartup.cs index 9c8a805..34090d3 100644 --- a/src/Web/WebApiStartup.cs +++ b/src/Web/WebApiStartup.cs @@ -31,7 +31,10 @@ namespace Geekbot.Web }) .ConfigureServices(services => { - services.AddControllers(); + services.AddControllers().AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + }); services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin",