diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs index 9d81293..1deaf8f 100644 --- a/DiscordBot/Program.cs +++ b/DiscordBot/Program.cs @@ -80,7 +80,7 @@ public class Program private static async Task LoadComponents(string[] args) { await Application.CreateApplication(); - + AppUpdater updater = new AppUpdater(); Update? update = await updater.PrepareUpdate(); if(update is not null) @@ -120,7 +120,8 @@ public class Program !Application.CurrentApplication.ApplicationEnvironmentVariables.ContainsKey("prefix")) await Installer.GenerateStartupConfig(); - + Application.InitializeThreadedApi(); + } } diff --git a/DiscordBotCore/API/ApiManager.cs b/DiscordBotCore/API/ApiManager.cs new file mode 100644 index 0000000..1505788 --- /dev/null +++ b/DiscordBotCore/API/ApiManager.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DiscordBotCore.API.Endpoints; +using DiscordBotCore.API.Endpoints.PluginManagement; +using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Others; +using Microsoft.AspNetCore.Builder; + +namespace DiscordBotCore.API; + +public class ApiManager +{ + private bool IsRunning { get; set; } + private List ApiEndpoints { get; } + + public ApiManager() + { + ApiEndpoints = new List(); + } + + public Result AddEndpoint(IEndpoint endpoint) + { + if (ApiEndpoints.Contains(endpoint) || ApiEndpoints.Exists(x => x.Path == endpoint.Path)) + { + return Result.Failure("Endpoint already exists"); + } + + ApiEndpoints.Add(endpoint); + return Result.Success(); + } + + public void RemoveEndpoint(string endpointPath) + { + this.ApiEndpoints.RemoveAll(endpoint => endpoint.Path == endpointPath); + } + + public bool EndpointExists(string endpointPath) + { + return this.ApiEndpoints.Exists(endpoint => endpoint.Path == endpointPath); + } + + internal void AddBaseEndpoints() + { + AddEndpoint(new HomeEndpoint()); + AddEndpoint(new PluginListEndpoint()); + } + + public async Task InitializeApi() + { + if (IsRunning) + return; + + IsRunning = true; + + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + app.UseRouting(); + + EndpointManager manager = new EndpointManager(app); + foreach(IEndpoint endpoint in this.ApiEndpoints) + { + manager.MapEndpoint(endpoint); + } + + await app.RunAsync(); + } +} diff --git a/DiscordBotCore/API/ApiResponse.cs b/DiscordBotCore/API/ApiResponse.cs new file mode 100644 index 0000000..fb0cdcd --- /dev/null +++ b/DiscordBotCore/API/ApiResponse.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using DiscordBotCore.Others; + +namespace DiscordBotCore.API; + +public class ApiResponse +{ + public string Message { get; set; } + public bool Success { get; set; } + + private ApiResponse(string message, bool success) + { + Message = message; + Success = success; + } + + public static ApiResponse From(string message, bool success) + { + return new ApiResponse(message, success); + } + + public static ApiResponse Fail(string message) + { + return new ApiResponse(message, false); + } + + public static ApiResponse Ok() + { + return new ApiResponse(string.Empty, true); + } + + public async Task ToJson() => await JsonManager.ConvertToJsonString(this); + +} diff --git a/DiscordBotCore/API/EndpointManager.cs b/DiscordBotCore/API/EndpointManager.cs new file mode 100644 index 0000000..1c4b9f9 --- /dev/null +++ b/DiscordBotCore/API/EndpointManager.cs @@ -0,0 +1,80 @@ +using System.IO; +using System.Text; +using DiscordBotCore.Interfaces.API; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace DiscordBotCore.API; + +internal sealed class EndpointManager +{ + private WebApplication _appBuilder; + internal EndpointManager(WebApplication appBuilder) + { + _appBuilder = appBuilder; + } + + internal void MapEndpoint(IEndpoint endpoint) + { + switch (endpoint.HttpMethod) + { + case EndpointType.Get: + _appBuilder.MapGet(endpoint.Path, async context => + { + //convert the context to a string + string jsonRequest = string.Empty; + if (context.Request.Body.CanRead) + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + jsonRequest = await reader.ReadToEndAsync(); + } + + var response = await endpoint.HandleRequest(jsonRequest); + await context.Response.WriteAsync(await response.ToJson()); + }); + break; + case EndpointType.Put: + _appBuilder.MapPut(endpoint.Path, async context => + { + string jsonRequest = string.Empty; + if (context.Request.Body.CanRead) + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + jsonRequest = await reader.ReadToEndAsync(); + } + + var response = await endpoint.HandleRequest(jsonRequest); + await context.Response.WriteAsync(await response.ToJson()); + }); + break; + case EndpointType.Post: + _appBuilder.MapPost(endpoint.Path, async context => + { + string jsonRequest = string.Empty; + if (context.Request.Body.CanRead) + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + jsonRequest = await reader.ReadToEndAsync(); + } + + var response = await endpoint.HandleRequest(jsonRequest); + await context.Response.WriteAsync(await response.ToJson()); + }); + break; + case EndpointType.Delete: + _appBuilder.MapDelete(endpoint.Path, async context => + { + string jsonRequest = string.Empty; + if (context.Request.Body.CanRead) + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + jsonRequest = await reader.ReadToEndAsync(); + } + + var response = await endpoint.HandleRequest(jsonRequest); + await context.Response.WriteAsync(await response.ToJson()); + }); + break; + } + } +} diff --git a/DiscordBotCore/API/Endpoints/HomeEndpoint.cs b/DiscordBotCore/API/Endpoints/HomeEndpoint.cs new file mode 100644 index 0000000..a06765b --- /dev/null +++ b/DiscordBotCore/API/Endpoints/HomeEndpoint.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Others; +using Microsoft.AspNetCore.Http; + +namespace DiscordBotCore.API.Endpoints; + +internal class HomeEndpoint : IEndpoint +{ + private static readonly string _HomeMessage = "Welcome to the DiscordBot API."; + public string Path => "/"; + EndpointType IEndpoint.HttpMethod => EndpointType.Get; + public async Task HandleRequest(string? jsonText) + { + string response = _HomeMessage; + if (jsonText != string.Empty) + { + var json = await JsonManager.ConvertFromJson>(jsonText!); + response += $"\n\nYou sent the following JSON:\n{string.Join("\n", json.Select(x => $"{x.Key}: {x.Value}"))}"; + } + + return ApiResponse.From(response, true); + } +} diff --git a/DiscordBotCore/API/Endpoints/PluginManagement/InstallPluginEndpoint.cs b/DiscordBotCore/API/Endpoints/PluginManagement/InstallPluginEndpoint.cs new file mode 100644 index 0000000..1298ad7 --- /dev/null +++ b/DiscordBotCore/API/Endpoints/PluginManagement/InstallPluginEndpoint.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Others; +using DiscordBotCore.Plugin; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace DiscordBotCore.API.Endpoints.PluginManagement; + +public class InstallPluginEndpoint : IEndpoint +{ + public string Path => "/api/plugin/install"; + public EndpointType HttpMethod => EndpointType.Put; + public async Task HandleRequest(string? jsonRequest) + { + Dictionary jsonDict = await JsonManager.ConvertFromJson>(jsonRequest); + string pluginName = jsonDict["pluginName"]; + + PluginOnlineInfo? pluginInfo = await Application.CurrentApplication.PluginManager.GetPluginDataByName(pluginName); + + if (pluginInfo == null) + { + return ApiResponse.Fail("Plugin not found."); + } + + await Application.CurrentApplication.PluginManager.InstallPlugin(pluginInfo, null); + return ApiResponse.Ok(); + } +} diff --git a/DiscordBotCore/API/Endpoints/PluginManagement/PluginListEndpoint.cs b/DiscordBotCore/API/Endpoints/PluginManagement/PluginListEndpoint.cs new file mode 100644 index 0000000..c78ef5b --- /dev/null +++ b/DiscordBotCore/API/Endpoints/PluginManagement/PluginListEndpoint.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Others; +using Microsoft.AspNetCore.Http; + +namespace DiscordBotCore.API.Endpoints.PluginManagement; + +public class PluginListEndpoint : IEndpoint +{ + public string Path => "/api/plugin/list/online"; + public EndpointType HttpMethod => EndpointType.Get; + public async Task HandleRequest(string? jsonRequest) + { + var onlineInfos = await Application.CurrentApplication.PluginManager.GetPluginsList(); + var response = await JsonManager.ConvertToJsonString(onlineInfos); + return ApiResponse.From(response, true); + } +} diff --git a/DiscordBotCore/API/Endpoints/PluginManagement/PluginListInstalledEndpoint.cs b/DiscordBotCore/API/Endpoints/PluginManagement/PluginListInstalledEndpoint.cs new file mode 100644 index 0000000..167df8c --- /dev/null +++ b/DiscordBotCore/API/Endpoints/PluginManagement/PluginListInstalledEndpoint.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Others; + +namespace DiscordBotCore.API.Endpoints.PluginManagement; + +public class PluginListInstalledEndpoint : IEndpoint +{ + public string Path => "/api/plugin/list/local"; + public EndpointType HttpMethod => EndpointType.Get; + public async Task HandleRequest(string? jsonRequest) + { + var listInstalled = await Application.CurrentApplication.PluginManager.GetInstalledPlugins(); + var response = await JsonManager.ConvertToJsonString(listInstalled); + return ApiResponse.From(response, true); + } +} diff --git a/DiscordBotCore/Application.cs b/DiscordBotCore/Application.cs index a399da9..4b90026 100644 --- a/DiscordBotCore/Application.cs +++ b/DiscordBotCore/Application.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; - +using DiscordBotCore.API; using DiscordBotCore.Bot; using DiscordBotCore.Interfaces.Logger; using DiscordBotCore.Online; @@ -45,6 +46,7 @@ namespace DiscordBotCore public InternalActionManager InternalActionManager { get; private set; } = null!; public PluginManager PluginManager { get; private set; } = null!; public ILogger Logger { get; private set; } = null!; + public ApiManager? ApiManager { get; private set; } /// /// Create the application. This method is used to initialize the application. Can not initialize multiple times. @@ -90,9 +92,32 @@ namespace DiscordBotCore CurrentApplication.InternalActionManager = new InternalActionManager(); await CurrentApplication.InternalActionManager.Initialize(); - + IsRunning = true; } + + /// + /// Initialize the API in a separate thread + /// + public static void InitializeThreadedApi() + { + if (CurrentApplication is null) + { + return; + } + + if(CurrentApplication.ApiManager is not null) + { + return; + } + + CurrentApplication.ApiManager = new ApiManager(); + CurrentApplication.ApiManager.AddBaseEndpoints(); + + Thread apiThread = new Thread(() => _ = CurrentApplication.ApiManager.InitializeApi()); + apiThread.Start(); + } + public static string GetResourceFullPath(string path) { var result = Path.Combine(_ResourcesFolder, path); diff --git a/DiscordBotCore/DiscordBotCore.csproj b/DiscordBotCore/DiscordBotCore.csproj index 8a00b2d..454fddc 100644 --- a/DiscordBotCore/DiscordBotCore.csproj +++ b/DiscordBotCore/DiscordBotCore.csproj @@ -1,19 +1,9 @@ - + net8.0 enable + Library - - 512 - portable - false - - - - - - - @@ -21,4 +11,7 @@ + + + \ No newline at end of file diff --git a/DiscordBotCore/Interfaces/API/IEndpoint.cs b/DiscordBotCore/Interfaces/API/IEndpoint.cs new file mode 100644 index 0000000..780a2d6 --- /dev/null +++ b/DiscordBotCore/Interfaces/API/IEndpoint.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using DiscordBotCore.API; +using DiscordBotCore.Others; + +namespace DiscordBotCore.Interfaces.API; + +public enum EndpointType +{ + Get, + Post, + Put, + Delete +} + +public interface IEndpoint +{ + public string Path { get; } + public EndpointType HttpMethod { get; } + public Task HandleRequest(string? jsonRequest); +} diff --git a/DiscordBotCore/Logging/Logger.cs b/DiscordBotCore/Logging/Logger.cs index 2e06e8d..617404d 100644 --- a/DiscordBotCore/Logging/Logger.cs +++ b/DiscordBotCore/Logging/Logger.cs @@ -18,7 +18,7 @@ public sealed class Logger : ILogger public Logger(string logFolder, string logMessageFormat, Action? outFunction = null) { this.LogMessageFormat = logMessageFormat; - var logFile = logFolder + DateTime.Now.ToString("yyyy-MM-dd") + ".log"; + var logFile = Path.Combine(logFolder, $"{DateTime.Now:yyyy-MM-dd}.log"); _LogFileStream = File.Open(logFile, FileMode.Append, FileAccess.Write, FileShare.Read); this._OutFunction = outFunction ?? DefaultLogFunction; } diff --git a/DiscordBotCore/Others/JsonManager.cs b/DiscordBotCore/Others/JsonManager.cs index 0aa71cb..347de0a 100644 --- a/DiscordBotCore/Others/JsonManager.cs +++ b/DiscordBotCore/Others/JsonManager.cs @@ -8,6 +8,20 @@ namespace DiscordBotCore.Others; public static class JsonManager { + + public static async Task ConvertToJsonString(T Data) + { + var str = new MemoryStream(); + await JsonSerializer.SerializeAsync(str, Data, typeof(T), new JsonSerializerOptions + { + WriteIndented = false, + }); + var result = Encoding.ASCII.GetString(str.ToArray()); + await str.FlushAsync(); + str.Close(); + return result; + } + /// /// Save to JSON file ///