From c79c792c43862cdb85db14b3b65b1b8e48830e90 Mon Sep 17 00:00:00 2001 From: Andrei Tudor Date: Wed, 18 Dec 2024 23:58:13 +0200 Subject: [PATCH] Added Socket support --- DiscordBot/Program.cs | 1 + DiscordBotCore/API/ConnectionDetails.cs | 16 +++ .../API/{ => Endpoints}/ApiManager.cs | 4 +- .../API/{ => Endpoints}/ApiResponse.cs | 2 +- .../API/{ => Endpoints}/EndpointManager.cs | 2 +- DiscordBotCore/API/Sockets/SocketManager.cs | 129 ++++++++++++++++++ DiscordBotCore/API/Sockets/SocketResponse.cs | 38 ++++++ .../Sockets/PluginDownloadProgressSocket.cs | 20 +++ DiscordBotCore/Application.cs | 26 +++- .../Interfaces/API/IConnectionDetails.cs | 7 + DiscordBotCore/Interfaces/API/IEndpoint.cs | 3 +- DiscordBotCore/Interfaces/API/ISocket.cs | 11 ++ .../Components/CustomTags/Marketplace.razor | 2 +- .../Pages/SidebarPages/PluginMarket.razor | 1 + 14 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 DiscordBotCore/API/ConnectionDetails.cs rename DiscordBotCore/API/{ => Endpoints}/ApiManager.cs (96%) rename DiscordBotCore/API/{ => Endpoints}/ApiResponse.cs (94%) rename DiscordBotCore/API/{ => Endpoints}/EndpointManager.cs (98%) create mode 100644 DiscordBotCore/API/Sockets/SocketManager.cs create mode 100644 DiscordBotCore/API/Sockets/SocketResponse.cs create mode 100644 DiscordBotCore/API/Sockets/Sockets/PluginDownloadProgressSocket.cs create mode 100644 DiscordBotCore/Interfaces/API/IConnectionDetails.cs create mode 100644 DiscordBotCore/Interfaces/API/ISocket.cs diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs index 1deaf8f..c171d53 100644 --- a/DiscordBot/Program.cs +++ b/DiscordBot/Program.cs @@ -121,6 +121,7 @@ public class Program await Installer.GenerateStartupConfig(); Application.InitializeThreadedApi(); + Application.InitializeThreadedSockets(); } diff --git a/DiscordBotCore/API/ConnectionDetails.cs b/DiscordBotCore/API/ConnectionDetails.cs new file mode 100644 index 0000000..9dacbc3 --- /dev/null +++ b/DiscordBotCore/API/ConnectionDetails.cs @@ -0,0 +1,16 @@ +using DiscordBotCore.Interfaces.API; + +namespace DiscordBotCore.API; + +public class ConnectionDetails : IConnectionDetails +{ + public string Host { get; } + public int Port { get; } + + public ConnectionDetails(string host, int port) + { + Host = host; + Port = port; + } + +} diff --git a/DiscordBotCore/API/ApiManager.cs b/DiscordBotCore/API/Endpoints/ApiManager.cs similarity index 96% rename from DiscordBotCore/API/ApiManager.cs rename to DiscordBotCore/API/Endpoints/ApiManager.cs index a069845..cb942c8 100644 --- a/DiscordBotCore/API/ApiManager.cs +++ b/DiscordBotCore/API/Endpoints/ApiManager.cs @@ -7,7 +7,7 @@ using DiscordBotCore.Interfaces.API; using DiscordBotCore.Others; using Microsoft.AspNetCore.Builder; -namespace DiscordBotCore.API; +namespace DiscordBotCore.API.Endpoints; public class ApiManager { @@ -52,7 +52,7 @@ public class ApiManager return this.ApiEndpoints.Exists(endpoint => endpoint.Path == endpointPath); } - public async Task InitializeApi() + public async void InitializeApi() { if (IsRunning) return; diff --git a/DiscordBotCore/API/ApiResponse.cs b/DiscordBotCore/API/Endpoints/ApiResponse.cs similarity index 94% rename from DiscordBotCore/API/ApiResponse.cs rename to DiscordBotCore/API/Endpoints/ApiResponse.cs index fb0cdcd..3ce78e9 100644 --- a/DiscordBotCore/API/ApiResponse.cs +++ b/DiscordBotCore/API/Endpoints/ApiResponse.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using DiscordBotCore.Others; -namespace DiscordBotCore.API; +namespace DiscordBotCore.API.Endpoints; public class ApiResponse { diff --git a/DiscordBotCore/API/EndpointManager.cs b/DiscordBotCore/API/Endpoints/EndpointManager.cs similarity index 98% rename from DiscordBotCore/API/EndpointManager.cs rename to DiscordBotCore/API/Endpoints/EndpointManager.cs index 1c4b9f9..fa4dc33 100644 --- a/DiscordBotCore/API/EndpointManager.cs +++ b/DiscordBotCore/API/Endpoints/EndpointManager.cs @@ -4,7 +4,7 @@ using DiscordBotCore.Interfaces.API; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -namespace DiscordBotCore.API; +namespace DiscordBotCore.API.Endpoints; internal sealed class EndpointManager { diff --git a/DiscordBotCore/API/Sockets/SocketManager.cs b/DiscordBotCore/API/Sockets/SocketManager.cs new file mode 100644 index 0000000..c63cf48 --- /dev/null +++ b/DiscordBotCore/API/Sockets/SocketManager.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using DiscordBotCore.API.Sockets.Sockets; +using DiscordBotCore.Interfaces.API; + +namespace DiscordBotCore.API.Sockets; + +internal class SocketManager +{ + private readonly IConnectionDetails _ConnectionDetails; + private List _Sockets = new List(); + + public SocketManager(IConnectionDetails connectionDetails) + { + _ConnectionDetails = connectionDetails; + } + + public void RegisterBaseSockets() + { + Register(new PluginDownloadProgressSocket()); + } + + public bool Register(ISocket socket) + { + if (_Sockets.Any(s => s.Path == socket.Path)) + { + return false; + } + + _Sockets.Add(socket); + return true; + } + + public void Start() + { + Console.WriteLine("Starting sockets ..."); + foreach (var socket in _Sockets) + { + Thread thread = new Thread(() => StartSocket(socket)); + thread.Start(); + } + } + + private async void StartSocket(ISocket socket) + { + + if (!socket.Path.StartsWith("/")) + { + throw new ArgumentException($"Socket path '{socket.Path}' must start with '/'."); + } + + string prefix = $"http://{_ConnectionDetails.Host}:{_ConnectionDetails.Port}{socket.Path}/"; + Console.WriteLine($"Starting socket with prefix: {prefix}"); + + HttpListener listener = new HttpListener(); + listener.Prefixes.Add(prefix); + listener.Start(); + + await ConnectionHandler(listener, socket.HandleRequest); + } + + + private async Task ConnectionHandler(HttpListener listener, Func> handler) + { + while (true) + { + var context = await listener.GetContextAsync(); + + if (context.Request.IsWebSocketRequest) + { + WebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null); + Application.CurrentApplication.Logger.Log("WebSocket connection established."); + await HandleSocket(webSocketContext.WebSocket, handler); + } + else { break; } + } + } + + private async Task HandleSocket(WebSocket socket, Func> handler) + { + if (socket.State != WebSocketState.Open) + { + return; + } + + byte[] buffer = new byte[1024 * 4]; + var receivedData = new List(); + WebSocketReceiveResult result; + + do + { + result = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + receivedData.AddRange(buffer.Take(result.Count)); + } while (!result.EndOfMessage); + + if (result.MessageType == WebSocketMessageType.Close) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection ...", CancellationToken.None); + Application.CurrentApplication.Logger.Log("WebSocket connection closed."); + return; + } + + Application.CurrentApplication.Logger.Log("WebSocket message received. Length: " + receivedData.Count); + SocketResponse socketResponse = await handler(receivedData.ToArray(), receivedData.Count); + ArraySegment response = new ArraySegment(socketResponse.Data, 0, socketResponse.Data.Length); + byte[]? lastResponse = null; + + while (!socketResponse.CloseConnectionAfterResponse) + { + if (lastResponse == null || !socketResponse.Data.SequenceEqual(lastResponse)) + { + await socket.SendAsync(response, WebSocketMessageType.Text, socketResponse.EndOfMessage, CancellationToken.None); + lastResponse = socketResponse.Data; + } + + socketResponse = await handler(receivedData.ToArray(), receivedData.Count); + response = new ArraySegment(socketResponse.Data, 0, socketResponse.Data.Length); + } + + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection ...", CancellationToken.None); + + + } +} diff --git a/DiscordBotCore/API/Sockets/SocketResponse.cs b/DiscordBotCore/API/Sockets/SocketResponse.cs new file mode 100644 index 0000000..9a31682 --- /dev/null +++ b/DiscordBotCore/API/Sockets/SocketResponse.cs @@ -0,0 +1,38 @@ + +namespace DiscordBotCore.API.Sockets; + +internal class SocketResponse +{ + public byte[] Data { get;} + public bool EndOfMessage { get; } + public bool Success { get; } + public bool CloseConnectionAfterResponse { get; set; } + + private SocketResponse(byte[] data, bool endOfMessage, bool success, bool closeConnectionAfterResponse) + { + Data = data; + EndOfMessage = endOfMessage; + Success = success; + CloseConnectionAfterResponse = closeConnectionAfterResponse; + } + + internal static SocketResponse From(byte[] data, bool endOfMessage, bool success, bool closeConnectionAfterResponse) + { + return new SocketResponse(data, endOfMessage, success, closeConnectionAfterResponse); + } + + internal static SocketResponse From(byte[] data, bool endOfMessage) + { + return new SocketResponse(data, endOfMessage, true, false); + } + + internal static SocketResponse From(byte[] data) + { + return new SocketResponse(data, true, true, false); + } + + internal static SocketResponse Fail() + { + return new SocketResponse(new byte[0], true, false, false); + } +} diff --git a/DiscordBotCore/API/Sockets/Sockets/PluginDownloadProgressSocket.cs b/DiscordBotCore/API/Sockets/Sockets/PluginDownloadProgressSocket.cs new file mode 100644 index 0000000..e1ca96a --- /dev/null +++ b/DiscordBotCore/API/Sockets/Sockets/PluginDownloadProgressSocket.cs @@ -0,0 +1,20 @@ +using System.Text; +using System.Threading.Tasks; +using DiscordBotCore.Interfaces.API; + +namespace DiscordBotCore.API.Sockets.Sockets; + +internal class PluginDownloadProgressSocket : ISocket +{ + private float value = 0.0f; + public string Path => "/plugin/download/progress"; + public Task HandleRequest(byte[] request, int count) + { + value += 0.1f; + string pluginName = Encoding.UTF8.GetString(request, 0, count); + Application.CurrentApplication.Logger.Log($"Received plugin download progress for {pluginName}."); + SocketResponse response = SocketResponse.From(Encoding.UTF8.GetBytes(value.ToString())); + response.CloseConnectionAfterResponse = value > 1.0f; + return Task.FromResult(response); + } +} diff --git a/DiscordBotCore/Application.cs b/DiscordBotCore/Application.cs index 4b90026..d8191e8 100644 --- a/DiscordBotCore/Application.cs +++ b/DiscordBotCore/Application.cs @@ -4,6 +4,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using DiscordBotCore.API; +using DiscordBotCore.API.Endpoints; +using DiscordBotCore.API.Sockets; using DiscordBotCore.Bot; using DiscordBotCore.Interfaces.Logger; using DiscordBotCore.Online; @@ -46,7 +48,8 @@ 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; } + internal ApiManager? ApiManager { get; private set; } + internal SocketManager? SocketManager { get; private set; } /// /// Create the application. This method is used to initialize the application. Can not initialize multiple times. @@ -113,9 +116,24 @@ namespace DiscordBotCore CurrentApplication.ApiManager = new ApiManager(); CurrentApplication.ApiManager.AddBaseEndpoints(); - - Thread apiThread = new Thread(() => _ = CurrentApplication.ApiManager.InitializeApi()); - apiThread.Start(); + CurrentApplication.ApiManager.InitializeApi(); + } + + public static void InitializeThreadedSockets() + { + if (CurrentApplication is null) + { + return; + } + + if(CurrentApplication.SocketManager is not null) + { + return; + } + + CurrentApplication.SocketManager = new SocketManager(new ConnectionDetails("localhost", 5055)); + CurrentApplication.SocketManager.RegisterBaseSockets(); + CurrentApplication.SocketManager.Start(); } public static string GetResourceFullPath(string path) diff --git a/DiscordBotCore/Interfaces/API/IConnectionDetails.cs b/DiscordBotCore/Interfaces/API/IConnectionDetails.cs new file mode 100644 index 0000000..79f0abc --- /dev/null +++ b/DiscordBotCore/Interfaces/API/IConnectionDetails.cs @@ -0,0 +1,7 @@ +namespace DiscordBotCore.Interfaces.API; + +public interface IConnectionDetails +{ + public string Host { get; } + public int Port { get; } +} diff --git a/DiscordBotCore/Interfaces/API/IEndpoint.cs b/DiscordBotCore/Interfaces/API/IEndpoint.cs index 780a2d6..05512fa 100644 --- a/DiscordBotCore/Interfaces/API/IEndpoint.cs +++ b/DiscordBotCore/Interfaces/API/IEndpoint.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; -using DiscordBotCore.API; -using DiscordBotCore.Others; +using DiscordBotCore.API.Endpoints; namespace DiscordBotCore.Interfaces.API; diff --git a/DiscordBotCore/Interfaces/API/ISocket.cs b/DiscordBotCore/Interfaces/API/ISocket.cs new file mode 100644 index 0000000..8fa06bf --- /dev/null +++ b/DiscordBotCore/Interfaces/API/ISocket.cs @@ -0,0 +1,11 @@ +using System.Net.WebSockets; +using System.Threading.Tasks; +using DiscordBotCore.API.Sockets; + +namespace DiscordBotCore.Interfaces.API; + +internal interface ISocket +{ + public string Path { get; } + public Task HandleRequest(byte[] request, int count); +} diff --git a/DiscordBotWebUI/Components/CustomTags/Marketplace.razor b/DiscordBotWebUI/Components/CustomTags/Marketplace.razor index d1f9c0c..c28aeb1 100644 --- a/DiscordBotWebUI/Components/CustomTags/Marketplace.razor +++ b/DiscordBotWebUI/Components/CustomTags/Marketplace.razor @@ -7,7 +7,7 @@ diff --git a/DiscordBotWebUI/Components/Pages/SidebarPages/PluginMarket.razor b/DiscordBotWebUI/Components/Pages/SidebarPages/PluginMarket.razor index 439cdeb..9a33941 100644 --- a/DiscordBotWebUI/Components/Pages/SidebarPages/PluginMarket.razor +++ b/DiscordBotWebUI/Components/Pages/SidebarPages/PluginMarket.razor @@ -20,6 +20,7 @@ }); if(!response.Success) { + Console.WriteLine(response.Message); return; }