diff --git a/src/Modix.Bot/Modules/GodboltDisasmModal.cs b/src/Modix.Bot/Modules/GodboltDisasmModal.cs new file mode 100644 index 000000000..f02cacd30 --- /dev/null +++ b/src/Modix.Bot/Modules/GodboltDisasmModal.cs @@ -0,0 +1,39 @@ +#nullable enable +using Discord; +using Discord.Interactions; + +namespace Modix.Bot.Modules +{ + public class GodboltDisasmModal : IModal + { + private string _language = "csharp"; + + public string Title => "Godbolt"; + + [InputLabel("Code")] + [ModalTextInput("code", TextInputStyle.Paragraph, initValue: """ + using System; + + class Program + { + static int Square(int num) => num * num; + static void Main() => Console.WriteLine(Square(42)); + } + """)] + public required string Code { get; set; } + + [InputLabel("Language: csharp, fsharp, vb or il")] + [ModalTextInput("language", initValue: "csharp")] + [RequiredInput(true)] + public required string Language + { + get => _language; + set => _language = value.ToLowerInvariant(); + } + + [InputLabel("Arguments")] + [ModalTextInput("arguments", TextInputStyle.Short)] + [RequiredInput(false)] + public string Arguments { get; set; } = ""; + } +} diff --git a/src/Modix.Bot/Modules/GodboltModule.cs b/src/Modix.Bot/Modules/GodboltModule.cs new file mode 100644 index 000000000..4270ccc08 --- /dev/null +++ b/src/Modix.Bot/Modules/GodboltModule.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using Discord; +using Discord.Interactions; +using Modix.Bot.Attributes; +using Modix.Services.CommandHelp; +using Modix.Services.Godbolt; + +namespace Modix.Bot.Modules +{ + [ModuleHelp("Godbolt", "Commands for working with Godbolt.")] + public class GodboltModule : InteractionModuleBase + { + private readonly GodboltService _godboltService; + + public GodboltModule(GodboltService godboltService) + { + _godboltService = godboltService; + } + + [SlashCommand("godbolt", description: "Compile and disassemble JIT code.")] + [DoNotDefer] + public Task DisasmAsync() => Context.Interaction.RespondWithModalAsync("godbolt_disasm"); + + [ModalInteraction("godbolt_disasm")] + public async Task ProcessDisasmAsync(GodboltDisasmModal modal) + { + try + { + var response = $""" + ```{modal.Language} + {modal.Code} + ``` + Disassembly{(string.IsNullOrWhiteSpace(modal.Arguments) ? "" : $" with ``{modal.Arguments}``")}: + ``` + {await _godboltService.CompileAsync(modal.Code, modal.Language, modal.Arguments)} + ``` + """; + + if (response.Length > DiscordConfig.MaxMessageSize) + { + await FollowupAsync("Error: The code is too long to be converted to a message.", allowedMentions: AllowedMentions.None); + return; + } + + await FollowupAsync(response, allowedMentions: AllowedMentions.None); + } + catch (Exception ex) + { + await FollowupAsync($"Error: {ex}", allowedMentions: AllowedMentions.None); + return; + } + } + } +} diff --git a/src/Modix.Services/Godbolt/AssemblyFilters.cs b/src/Modix.Services/Godbolt/AssemblyFilters.cs new file mode 100644 index 000000000..1adc3332d --- /dev/null +++ b/src/Modix.Services/Godbolt/AssemblyFilters.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace Modix.Services.Godbolt +{ + internal record class AssemblyFilters + { + public bool Binary { get; set; } = false; + public bool CommentOnly { get; set; } = true; + public bool Directives { get; set; } = true; + public bool Labels { get; set; } = true; + public bool Trim { get; set; } = true; + public bool Demangle { get; set; } = true; + public bool Intel { get; set; } = true; + public bool Execute { get; set; } = false; + } +} diff --git a/src/Modix.Services/Godbolt/CompileRequest.cs b/src/Modix.Services/Godbolt/CompileRequest.cs new file mode 100644 index 000000000..21642c6a1 --- /dev/null +++ b/src/Modix.Services/Godbolt/CompileRequest.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Modix.Services.Godbolt +{ + internal record class CompileRequest(string Lang, string Source) + { + public string Compiler { get; } = $"dotnettrunk{Lang}coreclr"; + public CompilerOptions Options { get; } = new(); + } +} diff --git a/src/Modix.Services/Godbolt/CompilerOptions.cs b/src/Modix.Services/Godbolt/CompilerOptions.cs new file mode 100644 index 000000000..624d7b755 --- /dev/null +++ b/src/Modix.Services/Godbolt/CompilerOptions.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Modix.Services.Godbolt +{ + internal record class CompilerOptions + { + public string UserArguments { get; set; } = ""; + public AssemblyFilters Filters { get; } = new(); + } +} diff --git a/src/Modix.Services/Godbolt/GodboltService.cs b/src/Modix.Services/Godbolt/GodboltService.cs new file mode 100644 index 000000000..097055353 --- /dev/null +++ b/src/Modix.Services/Godbolt/GodboltService.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Modix.Services.Godbolt +{ + public class GodboltService + { + private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private const string GodboltApiScheme = "https://godbolt.org/api/compiler/dotnettrunk{0}coreclr/compile"; + + public GodboltService(IHttpClientFactory httpClientFactory) + { + HttpClientFactory = httpClientFactory; + } + + public async Task CompileAsync(string code, string lang, string arguments) + { + var request = new CompileRequest(lang, code); + + if (!string.IsNullOrWhiteSpace(arguments)) + { + request.Options.UserArguments = arguments; + } + + var requestJson = JsonSerializer.Serialize(request, _jsonOptions); + + var client = HttpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Add("Accept", "text/plain"); + + var response = await client.PostAsync(string.Format(GodboltApiScheme, lang), new StringContent(requestJson, Encoding.UTF8, "application/json")); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Godbolt returns '{response.StatusCode}' for the request."); + } + + return await response.Content.ReadAsStringAsync(); + } + + protected IHttpClientFactory HttpClientFactory { get; } + } +} diff --git a/src/Modix/Extensions/ServiceCollectionExtensions.cs b/src/Modix/Extensions/ServiceCollectionExtensions.cs index 0ad55a812..aca28ef51 100644 --- a/src/Modix/Extensions/ServiceCollectionExtensions.cs +++ b/src/Modix/Extensions/ServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ using Modix.Services.Core; using Modix.Services.Csharp; using Modix.Services.EmojiStats; +using Modix.Services.Godbolt; using Modix.Services.GuildStats; using Modix.Services.Images; using Modix.Services.Moderation; @@ -174,6 +175,7 @@ public static IServiceCollection AddModix( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped, PromotionLoggingHandler>();