Add initial interaction support

This commit is contained in:
Daan Boerlage 2021-09-19 16:11:06 +02:00
parent 85d06b76e0
commit d81fb2a3d9
Signed by: daan
GPG key ID: FCE070E1E4956606
20 changed files with 336 additions and 1 deletions

View file

@ -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<IActionResult> 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<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) => 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<bool> 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);
}
}
}

View file

@ -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,
}
}

View file

@ -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<InteractionOption> Options { get; init; }
public bool DefaultPermission { get; init; }
[Required]
public int Version { get; set; }
}
}

View file

@ -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<InteractionOption> Options { get; set; }
public string TargetId { get; set; }
}
}

View file

@ -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<InteractionOption> Options { get; set; }
}
}

View file

@ -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<string, User> Users { get; set; }
public Dictionary<string, Member> Members { get; set; }
public Dictionary<string, Roles> Roles { get; set; }
public Dictionary<string, Channel> Channels { get; set; }
// public Dictionary<string, IMessage> Messages { get; set; }
}
}

View file

@ -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; }
}
}

View file

@ -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<string> Embeds { get; set; }
public AllowedMentions AllowedMentions { get; set; }
public int Flags { get; set; }
public List<Component> Components { get; set; }
}
}

View file

@ -0,0 +1,11 @@
namespace Geekbot.Web.Controllers.Interactions.Model
{
public enum InteractionResponseType
{
Pong = 1,
ChannelMessageWithSource = 4,
DeferredChannelMessageWithSource = 5,
DeferredUpdateMessage = 6,
UpdateMessage = 7,
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.Web.Controllers.Interactions.Model
{
public enum InteractionType
{
Ping = 1,
ApplicationCommand = 2,
MessageComponent = 3,
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Web.Controllers.Interactions.Model.MessageComponents
{
public record Component
{
}
}

View file

@ -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; }
}
}

View file

@ -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,
}
}

View file

@ -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<string> Roles { get; set; }
public DateTime JoinedAt { get; set; }
public DateTime PremiumSince { get; set; }
public bool Pending { get; set; }
public string Permissions { get; set; }
}
}

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -16,4 +16,8 @@
<ProjectReference Include="..\Core\Core.csproj" /> <ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Sodium.Core" Version="1.2.3" />
</ItemGroup>
</Project> </Project>

View file

@ -31,7 +31,10 @@ namespace Geekbot.Web
}) })
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddControllers(); services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
services.AddCors(options => services.AddCors(options =>
{ {
options.AddPolicy("AllowSpecificOrigin", options.AddPolicy("AllowSpecificOrigin",