From a6488522925c824b7b7884f3104fb4282295669c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Mon, 22 Sep 2025 00:29:51 -0500 Subject: [PATCH] Implement SDK using dependency injection - API requests are now made through the ApiRequestBuilder with DI supplied via the ApiRequestFactory singleton - Reliance on RestSharp and WebClient has been removed in favor of HttpClient - Auth token is now being tracked by the ITokenProvider - Network information (MAC address, broadcast addresses, IP address) is now supplied with the INetworkInformationProvider singleton - Connection state is now being maintained by the ConnectionService, with a reliance on RPC (SignalR websocket) reporting actual connection state without relying on pings - All download streams are now provided as a TrackableStream - Singleton Client class has been removed. All usage of the SDK should happen via Dependency Injection This is just a base, non-functional refactor of the SDK to use DI. The following changes to the rest of the codebase need to be made: - Usage of the SDK client in the launcher needs to be replaced in favor of injecting SDK services - PowerShell cmdlets need to be able to have services injected. Most likely a separate scope will have to be opened up per PS runtime? - Usage of the SDK client in the server needs to be replaced in favor of injecting SDK services. This should be minimal and should actually provide benefit when it comes to executing client-like features (scripts mostly) without needing a fully configured client that maintains connection state. - Configuration of the client needs to be implemented using ILANCommanderConfiguration --- .../ILANCommanderConfiguration.cs | 17 + .../INetworkInformationProvider.cs | 14 + .../Abstractions/ITokenProvider.cs | 9 + LANCommander.SDK/Client.cs | 1039 ----------------- .../LANCommanderConfiguration.cs | 17 + .../DownloadProgressChangedEventArgs.cs | 7 + .../Exceptions/InvalidAddressException.cs | 8 + .../IServiceCollectionExtensions.cs | 40 + .../Factories/ApiRequestFactory.cs | 13 + .../ProcessExecutionContextFactory.cs | 16 + LANCommander.SDK/Helpers/ApiRequestBuilder.cs | 322 +++++ LANCommander.SDK/Helpers/VersionHelper.cs | 12 + LANCommander.SDK/Models/ApiResponseMessage.cs | 9 + .../PowerShell/Cmdlets/Get-UserCustomField.cs | 5 +- .../PowerShell/Cmdlets/Out-PlayerAvatar.cs | 11 +- .../Cmdlets/Update-UserCustomField.cs | 10 +- .../PowerShell/Cmdlets/_BaseCmdlet.cs | 9 +- .../PowerShell/PowerShellScript.cs | 2 +- LANCommander.SDK/ProcessExecutionContext.cs | 44 +- .../Providers/NetworkInformationProvider.cs | 72 ++ LANCommander.SDK/Providers/TokenProvider.cs | 18 + LANCommander.SDK/Rpc/Chat.cs | 14 +- .../Rpc/Interfaces/Client/KeepAlive.cs | 8 + .../Rpc/Interfaces/Client/_IRpcClient.cs | 7 +- LANCommander.SDK/Rpc/KeepAlive.cs | 15 + LANCommander.SDK/Rpc/RpcClient.cs | 30 +- .../Services/AuthenticationService.cs | 186 +++ LANCommander.SDK/Services/BeaconService.cs | 76 +- LANCommander.SDK/Services/ChatService.cs | 18 +- .../Services/ConnectionService.cs | 103 ++ LANCommander.SDK/Services/DepotService.cs | 36 +- LANCommander.SDK/Services/GameService.cs | 415 ++++--- .../Services/IConnectionService.cs | 18 + LANCommander.SDK/Services/IssueService.cs | 54 +- LANCommander.SDK/Services/LauncherService.cs | 38 +- LANCommander.SDK/Services/LibraryService.cs | 52 +- LANCommander.SDK/Services/LobbyService.cs | 22 +- LANCommander.SDK/Services/MediaService.cs | 48 +- .../Services/PlaySessionService.cs | 31 +- LANCommander.SDK/Services/ProfileService.cs | 103 +- .../Services/RedistributableService.cs | 97 +- LANCommander.SDK/Services/SaveService.cs | 122 +- LANCommander.SDK/Services/ScriptService.cs | 199 ++-- LANCommander.SDK/Services/ServerService.cs | 64 +- LANCommander.SDK/Services/TagService.cs | 42 +- LANCommander.SDK/TrackableStream.cs | 39 +- 46 files changed, 1714 insertions(+), 1817 deletions(-) create mode 100644 LANCommander.SDK/Abstractions/ILANCommanderConfiguration.cs create mode 100644 LANCommander.SDK/Abstractions/INetworkInformationProvider.cs create mode 100644 LANCommander.SDK/Abstractions/ITokenProvider.cs delete mode 100644 LANCommander.SDK/Client.cs create mode 100644 LANCommander.SDK/Configuration/LANCommanderConfiguration.cs create mode 100644 LANCommander.SDK/DownloadProgressChangedEventArgs.cs create mode 100644 LANCommander.SDK/Exceptions/InvalidAddressException.cs create mode 100644 LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs create mode 100644 LANCommander.SDK/Factories/ApiRequestFactory.cs create mode 100644 LANCommander.SDK/Factories/ProcessExecutionContextFactory.cs create mode 100644 LANCommander.SDK/Helpers/ApiRequestBuilder.cs create mode 100644 LANCommander.SDK/Helpers/VersionHelper.cs create mode 100644 LANCommander.SDK/Models/ApiResponseMessage.cs create mode 100644 LANCommander.SDK/Providers/NetworkInformationProvider.cs create mode 100644 LANCommander.SDK/Providers/TokenProvider.cs create mode 100644 LANCommander.SDK/Rpc/Interfaces/Client/KeepAlive.cs create mode 100644 LANCommander.SDK/Rpc/KeepAlive.cs create mode 100644 LANCommander.SDK/Services/AuthenticationService.cs create mode 100644 LANCommander.SDK/Services/ConnectionService.cs create mode 100644 LANCommander.SDK/Services/IConnectionService.cs diff --git a/LANCommander.SDK/Abstractions/ILANCommanderConfiguration.cs b/LANCommander.SDK/Abstractions/ILANCommanderConfiguration.cs new file mode 100644 index 00000000..162a6b4d --- /dev/null +++ b/LANCommander.SDK/Abstractions/ILANCommanderConfiguration.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Humanizer.Bytes; + +namespace LANCommander.SDK.Abstractions; + +public interface ILANCommanderConfiguration +{ + public Uri BaseAddress { get; set; } + public bool OfflineMode { get; set; } + public bool DebugScripts { get; set; } + public int BeaconPort { get; set; } + public long UploadChunkSize { get; set; } + public IEnumerable InstallDirectories { get; set; } + public int IPXRelayPort { get; set; } + public string IPXRelayHost { get; set; } +} \ No newline at end of file diff --git a/LANCommander.SDK/Abstractions/INetworkInformationProvider.cs b/LANCommander.SDK/Abstractions/INetworkInformationProvider.cs new file mode 100644 index 00000000..939f5ab6 --- /dev/null +++ b/LANCommander.SDK/Abstractions/INetworkInformationProvider.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; + +namespace LANCommander.SDK.Abstractions; + +public interface INetworkInformationProvider +{ + public string GetMacAddress(); + public string GetComputerName(); + public string GetIpAddress(); + public IEnumerable GetNetworkInterfaces(); + public IEnumerable GetBroadcastAddresses(); +} \ No newline at end of file diff --git a/LANCommander.SDK/Abstractions/ITokenProvider.cs b/LANCommander.SDK/Abstractions/ITokenProvider.cs new file mode 100644 index 00000000..279ce7bd --- /dev/null +++ b/LANCommander.SDK/Abstractions/ITokenProvider.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace LANCommander.SDK.Abstractions; + +public interface ITokenProvider +{ + void SetToken(string token); + string GetToken(); +} \ No newline at end of file diff --git a/LANCommander.SDK/Client.cs b/LANCommander.SDK/Client.cs deleted file mode 100644 index 509f5fd9..00000000 --- a/LANCommander.SDK/Client.cs +++ /dev/null @@ -1,1039 +0,0 @@ -using LANCommander.SDK.Models; -using LANCommander.SDK.PowerShell.Cmdlets; -using LANCommander.SDK.Services; -using LANCommander.SDK.Extensions; -using LANCommander.SDK.Exceptions; - -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using RestSharp; -using RestSharp.Interceptors; -using Semver; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.NetworkInformation; -using System.Reflection; -using System.ServiceModel.Channels; -using System.Threading.Tasks; -using LANCommander.SDK.Rpc; - -namespace LANCommander.SDK -{ - public class Client - { - private readonly ILogger Logger; - - private RestClient ApiClient; - private AuthToken Token; - - private bool Connected = false; - private bool IgnoreVersion = false; - - public Uri BaseUrl; - public string DefaultInstallDirectory; - - public readonly GameService Games; - public readonly LibraryService Library; - public readonly DepotService Depot; - public readonly SaveService Saves; - public readonly RedistributableService Redistributables; - public readonly ScriptService Scripts; - public readonly ProfileService Profile; - public readonly MediaService Media; - public readonly LauncherService Launcher; - public readonly IssueService Issues; - public readonly LobbyService Lobbies; - public readonly ServerService Servers; - public readonly PlaySessionService PlaySessions; - public readonly TagService Tags; - public readonly BeaconService Beacon; - public readonly ChatService Chat; - public readonly RpcClient RPC; - - private Settings _Settings { get; set; } - public Settings Settings - { - get - { - if (_Settings == null) - _Settings = GetSettings(); - - return _Settings; - } - } - - public EventHandler OnError; - - public delegate void OnInstallProgressUpdateHandler(InstallProgress e); - public event OnInstallProgressUpdateHandler OnInstallProgressUpdate; - - public Client(string baseUrl, string defaultInstallDirectory) - { - DefaultInstallDirectory = defaultInstallDirectory; - - Games = new GameService(this, DefaultInstallDirectory); - Library = new LibraryService(this); - Depot = new DepotService(this); - Saves = new SaveService(this); - Redistributables = new RedistributableService(this); - Scripts = new ScriptService(this); - Profile = new ProfileService(this); - Media = new MediaService(this); - Launcher = new LauncherService(this); - Issues = new IssueService(this); - Lobbies = new LobbyService(this); - Servers = new ServerService(this); - PlaySessions = new PlaySessionService(this); - Tags = new TagService(this); - Beacon = new BeaconService(this); - Chat = new ChatService(this); - - BaseCmdlet.Client = this; - - try - { - ConfigureServerAddress(baseUrl); - } - catch - { - } - } - - public Client(string baseUrl, string defaultInstallDirectory, ILogger logger) - { - try - { - ConfigureServerAddress(baseUrl); - } - catch - { - } - - DefaultInstallDirectory = defaultInstallDirectory; - - Games = new GameService(this, DefaultInstallDirectory, logger); - Library = new LibraryService(this, logger); - Depot = new DepotService(this, logger); - Saves = new SaveService(this, logger); - Redistributables = new RedistributableService(this, logger); - Scripts = new ScriptService(this, logger); - Profile = new ProfileService(this, logger); - Media = new MediaService(this, logger); - Launcher = new LauncherService(this); - Issues = new IssueService(this); - Lobbies = new LobbyService(this, logger); - Servers = new ServerService(this, logger); - PlaySessions = new PlaySessionService(this, logger); - Tags = new TagService(this, logger); - Beacon = new BeaconService(this, logger); - Chat = new ChatService(this); - RPC = new RpcClient(this); - - BaseCmdlet.Client = this; - - Logger = logger; - } - - // Constructor for tests - internal Client(HttpClient httpClient, string defaultInstallDirectory) - { - ApiClient = new RestClient(httpClient); - - DefaultInstallDirectory = defaultInstallDirectory; - - Games = new GameService(this, DefaultInstallDirectory); - Library = new LibraryService(this); - Depot = new DepotService(this); - Saves = new SaveService(this); - Redistributables = new RedistributableService(this); - Scripts = new ScriptService(this); - Profile = new ProfileService(this); - Media = new MediaService(this); - Launcher = new LauncherService(this); - Issues = new IssueService(this); - Lobbies = new LobbyService(this); - Servers = new ServerService(this); - PlaySessions = new PlaySessionService(this); - Tags = new TagService(this); - RPC = new RpcClient(this); - - IgnoreVersion = true; - - BaseCmdlet.Client = this; - } - - public void ConfigureServerAddress(string baseUrl) - { - if (!String.IsNullOrWhiteSpace(baseUrl)) - { - var urisToTry = baseUrl.SuggestValidUris(); - - foreach (var uri in urisToTry) - { - Logger?.LogInformation("Attempting to configure server at {ServerAddress}", uri.ToString()); - - try - { - ApiClient = new RestClient(uri); - BaseUrl = uri; - - // Successful! Found our service - Logger?.LogInformation("Using server address {ServerAddress}", uri.ToString()); - - return; - } - catch - { - Logger?.LogError("Could not configure server at {ServerAddress}", uri.ToString()); - } - } - - throw new Exception("Could not configure a server at that address"); - } - } - - public async Task ChangeServerAddressAsync(string baseUrl) - { - if (!String.IsNullOrWhiteSpace(baseUrl)) - { - var urisToTry = baseUrl.SuggestValidUris(); - - // if url is fully qualified, limit specific urls - if (Uri.TryCreate(baseUrl, UriKind.RelativeOrAbsolute, out var baseUri)) - { - var hasPort = baseUrl.Replace(Uri.SchemeDelimiter, "").Contains(':'); - if (hasPort) - { - urisToTry = urisToTry.Take(baseUri.IsAbsoluteUri ? 1 : 2); - } - } - - foreach (var uri in urisToTry) - { - Logger?.LogInformation("Attempting to find server at {ServerAddress}", uri.ToString()); - - try - { - ApiClient = new RestClient(uri); - - if (await PingAsync()) - { - BaseUrl = uri; - - // Successful! Found our service - Logger?.LogInformation("Using server address {ServerAddress}", uri.ToString()); - - await RPC.ConnectAsync(); - - return; - } - } - catch - { - Logger?.LogError("Did not find server at {ServerAddress}", uri.ToString()); - } - } - - throw new Exception("Could not find a server at that address"); - } - } - - public bool IsConfigured() - { - return ApiClient != null && !string.IsNullOrWhiteSpace(BaseUrl?.ToString()); - } - - public bool IsConnected() - { - return Connected; - } - - public static SemVersion GetCurrentVersion() - { - return SemVersion.FromVersion(Assembly.GetExecutingAssembly().GetName().Version); - } - - internal T PostRequest(string route, object body, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddJsonBody(body) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Post(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal T PostRequest(string route, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Post(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task PostRequestAsync(string route, object body, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddJsonBody(body) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PostAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task PostRequestAsync(string route, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PostAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task PutRequestAsync(string route, object body, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.PutAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal T GetRequest(string route, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = ApiClient.Get(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task GetRequestAsync(string route, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.GetAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task DeleteRequestAsync(string route, bool ignoreVersion = false) - { - try - { - if (Token == null) - return default; - - var request = new RestRequest(route) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.DeleteAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task DownloadRequestAsync(string route, Action progressHandler, Action completeHandler) - { - try - { - route = route.TrimStart('/'); - - var client = new WebClient(); - var tempFile = Path.GetTempFileName(); - - client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); - client.DownloadProgressChanged += (s, e) => progressHandler(e); - client.DownloadFileCompleted += (s, e) => completeHandler(e); - - try - { - await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), tempFile); - } - catch (Exception ex) - { - Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", route); - - if (File.Exists(tempFile)) - File.Delete(tempFile); - - tempFile = String.Empty; - } - - return tempFile; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return null; - } - } - - internal async Task DownloadRequestAsync(string route, string destination) - { - try - { - route = route.TrimStart('/'); - - var client = new WebClient(); - - client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); - - try - { - await client.DownloadFileTaskAsync(new Uri(BaseUrl, route), destination); - } - catch (Exception ex) - { - Logger?.LogError(ex, "An unknown error occurred while downloading from the server at route {Route}", - route); - - if (File.Exists(destination)) - File.Delete(destination); - - destination = String.Empty; - } - - return destination; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return null; - } - } - - internal TrackableStream StreamRequest(string route) - { - route = route.TrimStart('/'); - - var client = new WebClient(); - - client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - client.Headers.Add("X-API-Version", GetCurrentVersion().ToString()); - - var ws = client.OpenRead(new Uri(BaseUrl, route)); - - return new TrackableStream(ws, true, Convert.ToInt64(client.ResponseHeaders["Content-Length"])); - } - - internal T UploadRequest(string route, string fileName, byte[] data, bool ignoreVersion = false) - { - try - { - var request = new RestRequest(route, Method.Post) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - request.AddFile(fileName, data, fileName); - - var response = ApiClient.Post(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task UploadRequestAsync(string route, Stream stream, bool ignoreVersion = false) - { - try - { - var request = new RestRequest(route, Method.Post) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - request.AddFile("File", () => stream, "File"); - - var response = await ApiClient.PostAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task UploadRequestAsync(string route, string fileName, byte[] data, bool ignoreVersion = false) - { - try - { - var request = new RestRequest(route, Method.Post) - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - request.AddFile(fileName, data, fileName); - - var response = await ApiClient.PostAsync(request); - - return response; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - internal async Task ChunkedUploadRequestAsync(string fileName, Stream stream, bool ignoreVersion = false) - { - try - { - var maxChunkSize = 1024 * 1024 * 50; - var initResponse = await PostRequestAsync("/Upload/Init", ignoreVersion); - - var buffer = new byte[maxChunkSize]; - - while (stream.Position < stream.Length) - { - var chunkRequest = new UploadChunkRequest(); - - chunkRequest.Start = stream.Position; - - if (stream.Position + maxChunkSize > stream.Length) - { - var bytes = stream.Length - stream.Position; - - buffer = new byte[bytes]; - - await stream.ReadAsync(buffer, 0, (int)(stream.Length - stream.Position)); - } - else - await stream.ReadAsync(buffer, 0, maxChunkSize); - - chunkRequest.End = stream.Position; - chunkRequest.Total = stream.Length; - chunkRequest.File = buffer; - chunkRequest.Key = initResponse.Key; - - await PostRequestAsync("/Upload/Chunk", chunkRequest, ignoreVersion); - } - - return initResponse.Key; - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - return default; - } - } - - public async Task AuthenticateAsync(string username, string password, bool ignoreVersion = false) - { - try - { - var request = new RestRequest("/api/Auth/Login", Method.Post); - - request.AddJsonBody(new AuthRequest() - { - UserName = username, - Password = password - }); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - var response = await ApiClient.ExecuteAsync(request); - - ErrorResponse errorResponse = null; - if (response.ResponseStatus == ResponseStatus.Error || response.ErrorException != null) - { - string message = response.ErrorMessage ?? response?.ErrorException.Message; - Logger?.LogError(response.ErrorException, "Authentication failed for user {UserName}: {Message}", username, message); - errorResponse = ParseErrorResponse(response); - } - - switch (response.StatusCode) - { - case HttpStatusCode.OK: - var token = new AuthToken - { - AccessToken = response.Data.AccessToken, - RefreshToken = response.Data.RefreshToken, - Expiration = response.Data.Expiration - }; - - UseToken(token); - - Connected = true; - - return token; - - case HttpStatusCode.Forbidden: - case HttpStatusCode.BadRequest: - case HttpStatusCode.Unauthorized: - Connected = false; - Logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username); - throw new AuthFailedException(AuthFailedException.AuthenticationErrorCode.InvalidCredentials, "Invalid username or password", errorData: errorResponse, innerException: response.ErrorException); - - default: - Connected = false; - Logger?.LogError("Authentication failed for user {UserName}: could not communicate with the server", username); - throw new WebException("Could not communicate with the server", innerException: response.ErrorException); - } - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - throw; - } - } - - public void Disconnect() - { - Connected = false; - } - - public async Task LogoutAsync() - { - await ApiClient.ExecuteAsync(new RestRequest("/api/Auth/Logout", Method.Post)); - - Connected = false; - Token = null; - } - - public async Task RegisterAsync(string username, string password, string passwordConfirmation) - { - try - { - var request = new RestRequest("/api/Auth/Register", Method.Post); - request.AddJsonBody(new AuthRequest() - { - UserName = username, - Password = password - }); - - var response = await ApiClient.ExecuteAsync(request); - - ErrorResponse errorResponse = null; - if (response.ResponseStatus == ResponseStatus.Error || response.ErrorException != null) - { - string message = response.ErrorMessage ?? response?.ErrorException.Message; - Logger?.LogError(response.ErrorException, "Registration failed for user {UserName}: {Message}", username, message); - errorResponse = ParseErrorResponse(response); - } - - switch (response.StatusCode) - { - case HttpStatusCode.OK: - Token = new AuthToken - { - AccessToken = response.Data.AccessToken, - RefreshToken = response.Data.RefreshToken, - Expiration = response.Data.Expiration - }; - - Connected = true; - - return Token; - - case HttpStatusCode.BadRequest: - case HttpStatusCode.Forbidden: - case HttpStatusCode.Unauthorized: - Connected = false; - throw new RegisterFailedException(response.Data.Message, errorData: errorResponse, innerException: response.ErrorException); - - default: - Connected = false; - Logger?.LogError("Registering failed for user {UserName}: could not communicate with the server", username); - throw new WebException("Could not communicate with the server", innerException: response.ErrorException); - } - } - catch (Exception ex) - { - OnError?.Invoke(this, ex); - - throw; - } - } - - public async Task> GetAuthenticationProvidersAsync() - { - var request = new RestRequest("/api/Auth/AuthenticationProviders") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - var response = await ApiClient.GetAsync>(request); - - return response; - } - - public string GetAuthenticationProviderLoginUrl(string provider) - { - return $"{BaseUrl}api/Auth/Login?Provider={provider}"; - } - - public bool Ping() - { - try - { - var guid = Guid.NewGuid().ToString(); - var request = new RestRequest("/api/Ping", Method.Head); - - request.AddHeader("X-Ping", guid); - - var response = ApiClient.Execute(request); - - return response.StatusCode == HttpStatusCode.OK && response.GetHeaderValue("X-Pong") == guid.FastReverse(); - } - catch - { - return false; - } - } - - public async Task PingAsync() - { - try - { - var guid = Guid.NewGuid().ToString(); - var request = new RestRequest("/api/Ping", Method.Head); - request.AddHeader("X-Ping", guid); - - // specify timeout for ping response - request.Timeout = TimeSpan.FromSeconds(4); - - var response = await ApiClient.ExecuteAsync(request); - - return response.StatusCode == HttpStatusCode.OK && response.GetHeaderValue("X-Pong") == guid.FastReverse(); - } - catch - { - return false; - } - } - - public bool ValidateToken() - { - return ValidateToken(Token); - } - - public bool ValidateToken(AuthToken token, bool ignoreVersion = false) - { - Logger?.LogTrace("Validating token..."); - - if (token == null) - { - Logger?.LogTrace("Token is null!"); - return false; - } - - var request = new RestRequest("/api/Auth/Validate") - .AddHeader("Authorization", $"Bearer {Token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - // specify timeout for auth response - request.Timeout = TimeSpan.FromSeconds(8); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken)) - { - Logger?.LogTrace("Token is empty!"); - return false; - } - - try - { - var response = ApiClient.Post(request); - - var valid = response.StatusCode == HttpStatusCode.OK; - - if (valid) - Logger?.LogTrace("Token is valid!"); - else - Logger?.LogTrace("Token is invalid!"); - - Connected = valid; - - return response.StatusCode == HttpStatusCode.OK; - } - catch (Exception ex) - { - Logger?.LogTrace(ex, "Token could not be retrieved"); - - return false; - } - } - - public async Task ValidateTokenAsync() - { - return await ValidateTokenAsync(Token); - } - - public async Task ValidateTokenAsync(AuthToken token, bool ignoreVersion = false) - { - Logger?.LogTrace("Validating token..."); - - if (token == null) - { - Logger?.LogTrace("Token is null!"); - return false; - } - - var request = new RestRequest("/api/Auth/Validate") - .AddHeader("Authorization", $"Bearer {token.AccessToken}") - .AddHeader("X-API-Version", GetCurrentVersion().ToString()); - - // specify timeout for auth response - request.Timeout = TimeSpan.FromSeconds(8); - - if (!ignoreVersion && !IgnoreVersion) - request.Interceptors = new List() { new VersionInterceptor() }; - - if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken)) - { - Logger?.LogTrace("Token is empty!"); - return false; - } - - try - { - var response = await ApiClient.PostAsync(request); - - Logger?.LogTrace("Token is valid!"); - - Connected = true; - } - catch (Exception ex) - { - Logger?.LogTrace(ex, "Could not validate token"); - - Connected = false; - } - - return Connected; - } - - public void UseToken(AuthToken token) - { - Token = token; - } - - public string GetServerAddress() - { - return BaseUrl.ToString(); - } - - public Settings GetSettings() - { - return GetRequest($"/api/Settings"); - } - - internal string GetMacAddress() - { - return NetworkInterface.GetAllNetworkInterfaces() - .Where(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(nic => nic.GetPhysicalAddress().ToString()) - .FirstOrDefault(); - } - - internal string GetIpAddress() - { - return Dns.GetHostEntry(Dns.GetHostName()).AddressList[0].ToString(); - } - - internal async Task GetIPXRelayHostAsync() - { - var host = Settings.IPXRelayHost; - - if (String.IsNullOrWhiteSpace(host)) - { - var serverAddress = new Uri(GetServerAddress()); - - host = serverAddress.DnsSafeHost; - } - - var entry = await Dns.GetHostEntryAsync(host); - - if (entry.AddressList.Length > 0) - host = entry.AddressList.First().ToString(); - - return host; - } - - internal ErrorResponse ParseErrorResponse(RestResponse response, bool defaultToGenericResponse = false) - { - ErrorResponse errorResponse = null; - - // Try to deserialize the error response. - try - { - errorResponse = JsonConvert.DeserializeObject(response.Content); - return errorResponse; - } - catch (Exception deserializationEx) - { - // Log error and create a fallback message if deserialization fails. - if (defaultToGenericResponse) - { - Logger?.LogError(deserializationEx, "Error deserializing error response for route {Route}", response.Request); - errorResponse = new ErrorResponse - { - Message = "Could not process the server response." - }; - } - } - - return errorResponse; - } - } -} diff --git a/LANCommander.SDK/Configuration/LANCommanderConfiguration.cs b/LANCommander.SDK/Configuration/LANCommanderConfiguration.cs new file mode 100644 index 00000000..654d8472 --- /dev/null +++ b/LANCommander.SDK/Configuration/LANCommanderConfiguration.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using LANCommander.SDK.Abstractions; + +namespace LANCommander.SDK.Configuration; + +public class LANCommanderConfiguration : ILANCommanderConfiguration +{ + public Uri BaseAddress { get; set; } + public bool OfflineMode { get; set; } + public bool DebugScripts { get; set; } + public int BeaconPort { get; set; } + public long UploadChunkSize { get; set; } + public IEnumerable InstallDirectories { get; set; } + public int IPXRelayPort { get; set; } + public string IPXRelayHost { get; set; } +} \ No newline at end of file diff --git a/LANCommander.SDK/DownloadProgressChangedEventArgs.cs b/LANCommander.SDK/DownloadProgressChangedEventArgs.cs new file mode 100644 index 00000000..18f565ac --- /dev/null +++ b/LANCommander.SDK/DownloadProgressChangedEventArgs.cs @@ -0,0 +1,7 @@ +namespace LANCommander.SDK; + +public class DownloadProgressChangedEventArgs +{ + public long BytesReceived { get; set; } + public long TotalBytes { get; set; } +} \ No newline at end of file diff --git a/LANCommander.SDK/Exceptions/InvalidAddressException.cs b/LANCommander.SDK/Exceptions/InvalidAddressException.cs new file mode 100644 index 00000000..659ef9af --- /dev/null +++ b/LANCommander.SDK/Exceptions/InvalidAddressException.cs @@ -0,0 +1,8 @@ +using System; + +namespace LANCommander.SDK.Exceptions; + +public class InvalidAddressException(string message) : Exception(message) +{ + +} \ No newline at end of file diff --git a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..be67d860 --- /dev/null +++ b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Configuration; +using LANCommander.SDK.Factories; +using LANCommander.SDK.Providers; +using LANCommander.SDK.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace LANCommander.SDK.Extensions; + +public static class IServiceCollectionExtensions +{ + public static IServiceCollection AddLANCommander(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Factories/ApiRequestFactory.cs b/LANCommander.SDK/Factories/ApiRequestFactory.cs new file mode 100644 index 00000000..f2cd616e --- /dev/null +++ b/LANCommander.SDK/Factories/ApiRequestFactory.cs @@ -0,0 +1,13 @@ +using System.Net.Http; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Helpers; + +namespace LANCommander.SDK.Factories; + +public class ApiRequestFactory(HttpClient httpClient, ITokenProvider tokenProvider, ILANCommanderConfiguration config) +{ + public ApiRequestBuilder Create() + { + return new ApiRequestBuilder(httpClient, tokenProvider, config); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Factories/ProcessExecutionContextFactory.cs b/LANCommander.SDK/Factories/ProcessExecutionContextFactory.cs new file mode 100644 index 00000000..bf387530 --- /dev/null +++ b/LANCommander.SDK/Factories/ProcessExecutionContextFactory.cs @@ -0,0 +1,16 @@ +using System; +using LANCommander.SDK.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Factories; + +public class ProcessExecutionContextFactory(IServiceProvider serviceProvider) +{ + public ProcessExecutionContext Create() + { + return new ProcessExecutionContext( + serviceProvider.GetService>(), + serviceProvider.GetService()); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Helpers/ApiRequestBuilder.cs b/LANCommander.SDK/Helpers/ApiRequestBuilder.cs new file mode 100644 index 00000000..06b152f0 --- /dev/null +++ b/LANCommander.SDK/Helpers/ApiRequestBuilder.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Models; +using LANCommander.SDK.Providers; +using RestSharp; +using RestSharp.Interceptors; +using Action = System.Action; + +namespace LANCommander.SDK.Helpers; + +public class ApiRequestBuilder( + HttpClient httpClient, + ITokenProvider tokenProvider, + ILANCommanderConfiguration config) +{ + private string _token { get; set; } + private bool _ignoreVersion { get; set; } + private object _body { get; set; } + private string _route { get; set; } + private HttpClient _httpClient { get; set; } + private HttpRequestMessage _request { get; set; } = new(); + private CancellationToken _cancellationToken { get; set; } = CancellationToken.None; + private Action _progressHandler { get; set; } + private Action _completeHandler { get; set; } + private Uri _baseAddress { get; set; } = config.BaseAddress; + + private ValueTask DeserializeResultAsync(HttpResponseMessage response) + { + return JsonSerializer.DeserializeAsync(response.Content.ReadAsStream(), cancellationToken: _cancellationToken); + } + + public ApiRequestBuilder UseAuthenticationToken() + { + _request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); + + return this; + } + + public ApiRequestBuilder UseRoute(string route) + { + _request.RequestUri = _baseAddress.Join(route); + + return this; + } + + public ApiRequestBuilder UseCancellationToken(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + + return this; + } + + public ApiRequestBuilder UseVersioning() + { + _request.Headers.Add("X-API-Version", VersionHelper.GetCurrentVersion().ToString()); + + // _request.Interceptors = new List() { new VersionInterceptor() }; + + return this; + } + + public ApiRequestBuilder UseMethod(HttpMethod method) + { + _request.Method = method; + + return this; + } + + public ApiRequestBuilder UseBaseAddress(Uri baseAddress) + { + _baseAddress = baseAddress; + + return this; + } + + public ApiRequestBuilder SetTimeout(TimeSpan timeout) + { + _httpClient.Timeout = timeout; + + return this; + } + + public ApiRequestBuilder AddBody(object body) + { + _request.Content = new StringContent(JsonSerializer.Serialize(body)); + + return this; + } + + public ApiRequestBuilder AddHeader(string key, string value) + { + _request.Headers.Add(key, value); + + return this; + } + + public ApiRequestBuilder OnProgress(Action progressHandler) + { + _progressHandler = progressHandler; + + return this; + } + + public ApiRequestBuilder OnComplete(Action completeHandler) + { + _completeHandler = completeHandler; + + return this; + } + + public async Task> SendAsync() where TResult : class + { + var response = (ApiResponseMessage)await _httpClient.SendAsync(_request, _cancellationToken); + + response.Data = await DeserializeResultAsync(response); + + return response; + } + + public async Task GetAsync() + { + _request.Method = HttpMethod.Get; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + response + .EnsureSuccessStatusCode(); + + return await DeserializeResultAsync(response); + } + + public async Task PostAsync() + { + _request.Method = HttpMethod.Post; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + response + .EnsureSuccessStatusCode(); + + return await DeserializeResultAsync(response); + } + + public async Task PutAsync() + { + _request.Method = HttpMethod.Put; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + response + .EnsureSuccessStatusCode(); + + return await DeserializeResultAsync(response); + } + + public async Task DeleteAsync() + { + _request.Method = HttpMethod.Delete; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + response + .EnsureSuccessStatusCode(); + + return await DeserializeResultAsync(response); + } + + public async Task HeadAsync() + { + _request.Method = HttpMethod.Head; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + response + .EnsureSuccessStatusCode(); + + return await DeserializeResultAsync(response); + } + + public async Task DownloadAsync(string destination) + { + _request.Method = HttpMethod.Get; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + using (var fs = new FileStream(destination, FileMode.Create)) + { + var responseStream = new TrackableStream(await response.Content.ReadAsStreamAsync(_cancellationToken)); + + if (_progressHandler != null) + responseStream.OnProgress += (position, length) => + { + _progressHandler.Invoke(new DownloadProgressChangedEventArgs + { + BytesReceived = position, + TotalBytes = length, + }); + }; + + if (_completeHandler != null) + responseStream.OnComplete += () => _completeHandler(); + + await responseStream.CopyToAsync(fs, _cancellationToken); + } + + return new FileInfo(destination); + } + + public async Task StreamAsync() + { + _request.Method = HttpMethod.Get; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + var stream = await response.Content.ReadAsStreamAsync(_cancellationToken);; + + var responseStream = new TrackableStream(stream); + + if (_progressHandler != null) + responseStream.OnProgress += (position, length) => + { + _progressHandler.Invoke(new DownloadProgressChangedEventArgs + { + BytesReceived = position, + TotalBytes = length, + }); + }; + + if (_completeHandler != null) + responseStream.OnComplete += () => _completeHandler(); + + return responseStream; + } + + public async Task UploadAsync(string fileName, byte[] data) + { + using (var form = new MultipartFormDataContent()) + { + var dataContent = new ByteArrayContent(data); + + form.Add(dataContent, "file", fileName); + + _request.Content = form; + _request.Method = HttpMethod.Post; + + var response = await _httpClient.SendAsync(_request, _cancellationToken); + + return await DeserializeResultAsync(response); + } + } + + public async Task UploadAsync(string fileName, Stream data) + { + var buffer = new byte[data.Length]; + + await data.ReadExactlyAsync(buffer, 0, buffer.Length, _cancellationToken); + + return await UploadAsync(fileName, buffer); + } + + public async Task UploadInChunksAsync(long chunkSize, Stream data) + { + try + { + var initResponse = await new ApiRequestBuilder(httpClient, tokenProvider, config) + .UseRoute("/Upload/Init") + .UseVersioning() + .UseAuthenticationToken() + .UseCancellationToken(_cancellationToken) + .PostAsync(); + + var buffer = new byte[chunkSize]; + + while (data.Position < data.Length) + { + var start = data.Position; + + if (data.Position + chunkSize > data.Length) + { + var remainingBytes = data.Length - data.Position; + + buffer = new byte[remainingBytes]; + } + + await data.ReadExactlyAsync(buffer, 0, buffer.Length, _cancellationToken); + + var chunkRequest = new UploadChunkRequest + { + Start = start, + End = data.Position, + File = buffer, + Key = initResponse.Key, + }; + + await new ApiRequestBuilder(httpClient, tokenProvider, config) + .AddBody(chunkRequest) + .UseRoute("/Upload/Chunk") + .UseVersioning() + .UseAuthenticationToken() + .UseCancellationToken(_cancellationToken) + .PostAsync(); + } + + return initResponse.Key; + } + catch (Exception ex) + { + return default; + } + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Helpers/VersionHelper.cs b/LANCommander.SDK/Helpers/VersionHelper.cs new file mode 100644 index 00000000..0f23a240 --- /dev/null +++ b/LANCommander.SDK/Helpers/VersionHelper.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using Semver; + +namespace LANCommander.SDK.Helpers; + +public static class VersionHelper +{ + public static SemVersion GetCurrentVersion() + { + return SemVersion.FromVersion(Assembly.GetExecutingAssembly().GetName().Version); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Models/ApiResponseMessage.cs b/LANCommander.SDK/Models/ApiResponseMessage.cs new file mode 100644 index 00000000..2b56a588 --- /dev/null +++ b/LANCommander.SDK/Models/ApiResponseMessage.cs @@ -0,0 +1,9 @@ +using System.Net.Http; + +namespace LANCommander.SDK.Models; + +public class ApiResponseMessage : HttpResponseMessage + where TResult : class +{ + public TResult Data { get; set; } +} \ No newline at end of file diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs index 8ebacf9e..2a239d7a 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs @@ -2,19 +2,20 @@ using System; using System.IO; using System.Management.Automation; +using LANCommander.SDK.Services; namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsCommon.Get, "UserCustomField")] [OutputType(typeof(string))] - public class GetUserCustomFieldCmdlet : BaseCmdlet + public class GetUserCustomFieldCmdlet(ProfileService profileService) : BaseCmdlet { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } protected override void ProcessRecord() { - var result = Client.Profile.GetCustomField(Name).GetAwaiter().GetResult(); + var result = profileService.GetCustomFieldAsync(Name).GetAwaiter().GetResult(); WriteObject(result); } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs index d7f1de41..3ef3fb2f 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs @@ -1,18 +1,15 @@ -using LANCommander.SDK.Extensions; -using LANCommander.SDK.Helpers; -using LANCommander.SDK.PowerShell.Models; -using System.Linq; -using System.Management.Automation; +using System.Management.Automation; +using LANCommander.SDK.Services; namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.Out, "PlayerAvatar")] [OutputType(typeof(string))] - public class OutPlayerAvatarCmdlet : BaseCmdlet + public class OutPlayerAvatarCmdlet(ProfileService profileService) : BaseCmdlet { protected override void ProcessRecord() { - var result = Client.Profile.GetAvatarAsync().GetAwaiter().GetResult(); + var result = profileService.GetAvatarAsync().GetAwaiter().GetResult(); WriteObject(result, false); } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs index 2e7f0494..eb3ab062 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs @@ -1,13 +1,11 @@ -using PeanutButter.INI; -using System; -using System.IO; -using System.Management.Automation; +using System.Management.Automation; +using LANCommander.SDK.Services; namespace LANCommander.SDK.PowerShell.Cmdlets { [Cmdlet(VerbsData.Update, "UserCustomField")] [OutputType(typeof(string))] - public class UpdateUserCustomFieldCmdlet : BaseCmdlet + public class UpdateUserCustomFieldCmdlet(ProfileService profileService) : BaseCmdlet { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } @@ -17,7 +15,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets protected override void ProcessRecord() { - var result = Client.Profile.UpdateCustomField(Name, Value).GetAwaiter().GetResult(); + var result = profileService.UpdateCustomFieldAsync(Name, Value).GetAwaiter().GetResult(); WriteObject(result); } diff --git a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs b/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs index b9dff0b1..fc3684ff 100644 --- a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs +++ b/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs @@ -1,15 +1,8 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Text; -using System.Threading.Tasks; +using System.Management.Automation; namespace LANCommander.SDK.PowerShell.Cmdlets { public abstract class BaseCmdlet : Cmdlet { - public static Client Client { get; set; } } } diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs index 83db98b3..a4bcf4ba 100644 --- a/LANCommander.SDK/PowerShell/PowerShellScript.cs +++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs @@ -52,7 +52,7 @@ namespace LANCommander.SDK.PowerShell DebugHandler = new PowerShellDebugHandler(); InitialSessionState = InitialSessionState.CreateDefault(); - + InitialSessionState.Commands.Add(new SessionStateCmdletEntry("Convert-AspectRatio", typeof(ConvertAspectRatioCmdlet), null)); InitialSessionState.Commands.Add(new SessionStateCmdletEntry("ConvertFrom-SerializedBase64", typeof(ConvertFromSerializedBase64Cmdlet), null)); InitialSessionState.Commands.Add(new SessionStateCmdletEntry("ConvertTo-SerializedBase64", typeof(ConvertToSerializedBase64Cmdlet), null)); diff --git a/LANCommander.SDK/ProcessExecutionContext.cs b/LANCommander.SDK/ProcessExecutionContext.cs index 9a06133a..652456e5 100644 --- a/LANCommander.SDK/ProcessExecutionContext.cs +++ b/LANCommander.SDK/ProcessExecutionContext.cs @@ -1,41 +1,27 @@ using LANCommander.SDK.Extensions; using LANCommander.SDK.Helpers; -using LANCommander.SDK.Models; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using LANCommander.SDK.Enums; -using YamlDotNet.Serialization; +using LANCommander.SDK.Services; namespace LANCommander.SDK { - public class ProcessExecutionContext : IDisposable + public class ProcessExecutionContext( + ILogger logger, + LobbyService lobbyService) : IDisposable { - private readonly Client Client; - private readonly ILogger Logger; - private Process Process; private Dictionary Variables { get; set; } = new Dictionary(); public event DataReceivedEventHandler? OutputDataReceived; public event DataReceivedEventHandler? ErrorDataReceived; - - public ProcessExecutionContext(Client client) - { - Client = client; - } - - public ProcessExecutionContext(Client client, ILogger logger) - { - Client = client; - Logger = logger; - } public void AddVariable(string key, string value) { @@ -64,7 +50,7 @@ namespace LANCommander.SDK } catch (Exception ex) { - Logger?.LogError(ex, "Could not expand runtime variables"); + logger?.LogError(ex, "Could not expand runtime variables"); return input; } @@ -102,10 +88,10 @@ namespace LANCommander.SDK if (OutputDataReceived != null && !processStartInfo.UseShellExecute) Process.ErrorDataReceived += ErrorDataReceived; - Logger?.LogTrace("Running server executable"); - Logger?.LogTrace("Arguments: {Arguments}", Process.StartInfo.Arguments); - Logger?.LogTrace("File Name: {FileName}", Process.StartInfo.FileName); - Logger?.LogTrace("Working Directory: {WorkingDirectory}", Process.StartInfo.WorkingDirectory); + logger?.LogTrace("Running server executable"); + logger?.LogTrace("Arguments: {Arguments}", Process.StartInfo.Arguments); + logger?.LogTrace("File Name: {FileName}", Process.StartInfo.FileName); + logger?.LogTrace("Working Directory: {WorkingDirectory}", Process.StartInfo.WorkingDirectory); bool exited = false; @@ -196,11 +182,11 @@ namespace LANCommander.SDK if (!String.IsNullOrWhiteSpace(args)) Process.StartInfo.Arguments += " " + args; - Logger?.LogTrace("Running game executable"); - Logger?.LogTrace("Arguments: {Arguments}", Process.StartInfo.Arguments); - Logger?.LogTrace("File Name: {FileName}", Process.StartInfo.FileName); - Logger?.LogTrace("Working Directory: {WorkingDirectory}", Process.StartInfo.WorkingDirectory); - Logger?.LogTrace("Manifest Path: {ManifestPath}", ManifestHelper.GetPath(installDirectory, gameId)); + logger?.LogTrace("Running game executable"); + logger?.LogTrace("Arguments: {Arguments}", Process.StartInfo.Arguments); + logger?.LogTrace("File Name: {FileName}", Process.StartInfo.FileName); + logger?.LogTrace("Working Directory: {WorkingDirectory}", Process.StartInfo.WorkingDirectory); + logger?.LogTrace("Manifest Path: {ManifestPath}", ManifestHelper.GetPath(installDirectory, gameId)); bool exited = false; @@ -223,7 +209,7 @@ namespace LANCommander.SDK } catch { } - Client.Lobbies.ReleaseSteam(); + lobbyService.ReleaseSteam(); } } } diff --git a/LANCommander.SDK/Providers/NetworkInformationProvider.cs b/LANCommander.SDK/Providers/NetworkInformationProvider.cs new file mode 100644 index 00000000..3dd9ad63 --- /dev/null +++ b/LANCommander.SDK/Providers/NetworkInformationProvider.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using LANCommander.SDK.Abstractions; + +namespace LANCommander.SDK.Providers; + +public class NetworkInformationProvider : INetworkInformationProvider +{ + public string GetMacAddress() + { + return NetworkInterface.GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up && nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .Select(nic => nic.GetPhysicalAddress().ToString()) + .FirstOrDefault(); + } + + public string GetComputerName() + { + return Dns.GetHostName(); + } + + public string GetIpAddress() + { + return Dns.GetHostEntry(Dns.GetHostName()).AddressList[0].ToString(); + } + + /// + /// Get active network interfaces on the system + /// + /// + public IEnumerable GetNetworkInterfaces() + { + var networkInterfaces = NetworkInterface + .GetAllNetworkInterfaces() + .Where(i => i.OperationalStatus == OperationalStatus.Up && + i.NetworkInterfaceType != NetworkInterfaceType.Loopback); + + return networkInterfaces; + } + + public IEnumerable GetBroadcastAddresses() + { + var networkInterfaces = GetNetworkInterfaces(); + + foreach (var nic in networkInterfaces) + { + foreach (var ua in nic.GetIPProperties().UnicastAddresses) + { + if (ua.Address.AddressFamily == AddressFamily.InterNetwork) + { + var ip = ua.Address; + var mask = ua.IPv4Mask; + + if (mask == null) + continue; + + var ipBytes = ip.GetAddressBytes(); + var maskBytes = mask.GetAddressBytes(); + var broadcastBytes = new byte[4]; + + for (var i = 0; i < 4; i++) + broadcastBytes[i] = (byte)(ipBytes[i] | (maskBytes[i] ^ 255)); + + yield return new IPAddress(broadcastBytes); + } + } + } + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Providers/TokenProvider.cs b/LANCommander.SDK/Providers/TokenProvider.cs new file mode 100644 index 00000000..5b31cbcf --- /dev/null +++ b/LANCommander.SDK/Providers/TokenProvider.cs @@ -0,0 +1,18 @@ +using LANCommander.SDK.Abstractions; + +namespace LANCommander.SDK.Providers; + +public class TokenProvider : ITokenProvider +{ + private string _token { get; set; } + + public void SetToken(string token) + { + _token = token; + } + + public string GetToken() + { + return _token; + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Chat.cs b/LANCommander.SDK/Rpc/Chat.cs index 01b8e6de..1f47aa73 100644 --- a/LANCommander.SDK/Rpc/Chat.cs +++ b/LANCommander.SDK/Rpc/Chat.cs @@ -1,33 +1,37 @@ using System; using System.Threading.Tasks; using LANCommander.SDK.Models; +using LANCommander.SDK.Services; +using Microsoft.Extensions.DependencyInjection; namespace LANCommander.SDK.Rpc; public partial class RpcClient { + private readonly ChatService _chatService = serviceProvider.GetService(); + public async Task Chat_AddedToThreadAsync(ChatThread thread) { - await client.Chat.AddedToThreadAsync(thread); + await _chatService.AddedToThreadAsync(thread); } public async Task Chat_ReceiveMessagesAsync(Guid threadId, ChatMessage[] messages) { - await client.Chat.ReceiveMessagesAsync(threadId, messages); + await _chatService.ReceiveMessagesAsync(threadId, messages); } public async Task Chat_ReceiveMessageAsync(Guid threadId, ChatMessage message) { - await client.Chat.ReceiveMessageAsync(threadId, message); + await _chatService.ReceiveMessageAsync(threadId, message); } public async Task Chat_StartTyping(Guid threadId, string userIdentifier) { - await client.Chat.StartTypingAsync(threadId, userIdentifier); + await _chatService.StartTypingAsync(threadId, userIdentifier); } public async Task Chat_StopTyping(Guid threadId, string userIdentifier) { - await client.Chat.StopTypingAsync(threadId, userIdentifier); + await _chatService.StopTypingAsync(threadId, userIdentifier); } } \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Interfaces/Client/KeepAlive.cs b/LANCommander.SDK/Rpc/Interfaces/Client/KeepAlive.cs new file mode 100644 index 00000000..96635f81 --- /dev/null +++ b/LANCommander.SDK/Rpc/Interfaces/Client/KeepAlive.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace LANCommander.SDK.Rpc.Client; + +public partial interface IRpcClient +{ + public bool IsConnected(); +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Interfaces/Client/_IRpcClient.cs b/LANCommander.SDK/Rpc/Interfaces/Client/_IRpcClient.cs index 2c87011a..2c8a900b 100644 --- a/LANCommander.SDK/Rpc/Interfaces/Client/_IRpcClient.cs +++ b/LANCommander.SDK/Rpc/Interfaces/Client/_IRpcClient.cs @@ -1,6 +1,11 @@ +using System.Threading.Tasks; +using LANCommander.SDK.Rpc.Server; + namespace LANCommander.SDK.Rpc.Client; public partial interface IRpcClient { - + public IRpcHub Server { get; set; } + public Task ConnectAsync(); + public Task DisconnectAsync(); } \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/KeepAlive.cs b/LANCommander.SDK/Rpc/KeepAlive.cs new file mode 100644 index 00000000..8580d546 --- /dev/null +++ b/LANCommander.SDK/Rpc/KeepAlive.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using LANCommander.SDK.Rpc.Client; +using LANCommander.SDK.Services; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; + +namespace LANCommander.SDK.Rpc; + +public partial class RpcClient : IRpcClient +{ + public bool IsConnected() + { + return _connection.State == HubConnectionState.Connected; + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/RpcClient.cs b/LANCommander.SDK/Rpc/RpcClient.cs index be4201f8..18b35d92 100644 --- a/LANCommander.SDK/Rpc/RpcClient.cs +++ b/LANCommander.SDK/Rpc/RpcClient.cs @@ -1,23 +1,27 @@ +using System; using System.Threading.Tasks; using LANCommander.SDK.Extensions; using LANCommander.SDK.Rpc.Client; using LANCommander.SDK.Rpc.Server; +using LANCommander.SDK.Services; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; namespace LANCommander.SDK.Rpc; -public partial class RpcClient(SDK.Client client) : IRpcClient +public partial class RpcClient(IServiceProvider serviceProvider) : IRpcClient { private HubConnection _connection = default!; + private readonly IConnectionService _connectionService = serviceProvider.GetService(); public IRpcHub Server { get; set; } = default!; - public async Task ConnectAsync() + public async Task ConnectAsync() { try { _connection = new HubConnectionBuilder() - .WithUrl(client.BaseUrl.Join("rpc")) + .WithUrl(_connectionService.GetServerAddress().Join("rpc")) .Build(); Server = _connection.ServerProxy(); @@ -25,10 +29,26 @@ public partial class RpcClient(SDK.Client client) : IRpcClient _ = _connection.ClientRegistration(this); await _connection.StartAsync(); + + return true; } catch { - + return false; } - } + } + + public async Task DisconnectAsync() + { + try + { + await _connection.StopAsync(); + + return true; + } + catch + { + return false; + } + } } \ No newline at end of file diff --git a/LANCommander.SDK/Services/AuthenticationService.cs b/LANCommander.SDK/Services/AuthenticationService.cs new file mode 100644 index 00000000..10052e52 --- /dev/null +++ b/LANCommander.SDK/Services/AuthenticationService.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Exceptions; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Factories; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Services; + +public class AuthenticationService( + ILogger logger, + ITokenProvider tokenProvider, + ApiRequestFactory apiRequestFactory, + IConnectionService connectionService) +{ + public async Task AuthenticateAsync(string username, string password) + { + try + { + var response = await apiRequestFactory + .Create() + .UseRoute("/api/Auth/Login") + .UseMethod(HttpMethod.Post) + .AddBody(new AuthRequest + { + UserName = username, + Password = password, + }) + .SendAsync(); + + ErrorResponse errorResponse = null; + + if (!response.IsSuccessStatusCode) + { + string message = response.ReasonPhrase; + + logger?.LogError("Authentication failed for user {UserName}: {Message}", username, message); + + errorResponse = ParseErrorResponse(response); + } + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + var token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + tokenProvider.SetToken(token.AccessToken); + + return token; + + case HttpStatusCode.Forbidden: + case HttpStatusCode.BadRequest: + case HttpStatusCode.Unauthorized: + logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username); + throw new AuthFailedException(AuthFailedException.AuthenticationErrorCode.InvalidCredentials, "Invalid username or password", errorData: errorResponse, innerException: response.ErrorException); + + default: + logger?.LogError("Authentication failed for user {UserName}: could not communicate with the server", username); + throw new WebException("Could not communicate with the server"); + } + } + catch (Exception ex) + { + // OnError?.Invoke(this, ex); + + throw; + } + } + + public async Task LogoutAsync() + { + await apiRequestFactory + .Create() + .UseRoute("/api/Auth/Logout") + .UseAuthenticationToken() + .PostAsync(); + + tokenProvider.SetToken(null); + + await connectionService.DisconnectAsync(); + } + + public async Task RegisterAsync(string username, string password, string passwordConfirmation) + { + try + { + var response = await apiRequestFactory + .Create() + .UseRoute("/api/Auth/Register") + .UseMethod(HttpMethod.Post) + .AddBody(new AuthRequest + { + UserName = username, + Password = password, + }) + .SendAsync(); + + ErrorResponse errorResponse = null; + + if (!response.IsSuccessStatusCode) + { + string message = response.ReasonPhrase; + + logger?.LogError("Registration failed for user {UserName}: {Message}", username, message); + + errorResponse = ParseErrorResponse(response); + } + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + tokenProvider.SetToken(null); + + return; + + case HttpStatusCode.BadRequest: + case HttpStatusCode.Forbidden: + case HttpStatusCode.Unauthorized: + throw new RegisterFailedException("Could not register user", errorData: errorResponse); + + default: + logger?.LogError("Registering failed for user {UserName}: could not communicate with the server", username); + throw new WebException("Could not communicate with the server"); + } + } + catch (Exception ex) + { + // OnError?.Invoke(this, ex); + + throw; + } + } + + public async Task> GetAuthenticationProvidersAsync() + { + return await apiRequestFactory + .Create() + .UseRoute("/api/Auth/GetAuthenticationProviders") + .UseVersioning() + .GetAsync>(); + } + + public Uri GetAuthenticationProviderLoginUrl(string provider) + { + return connectionService.GetServerAddress().Join($"api/Auth/Login?Provider={provider}"); + } + + internal ErrorResponse ParseErrorResponse(bool defaultToGenericResponse = false) + { + ErrorResponse errorResponse = null; + + // Try to deserialize the error response. + try + { + errorResponse = JsonSerializer.Deserialize(response.Content); + + return errorResponse; + } + catch (Exception deserializationEx) + { + // Log error and create a fallback message if deserialization fails. + if (defaultToGenericResponse) + { + logger?.LogError(deserializationEx, "Error deserializing error response for route {Route}", response.Request); + + errorResponse = new ErrorResponse + { + Message = "Could not process the server response." + }; + } + } + + return errorResponse; + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Services/BeaconService.cs b/LANCommander.SDK/Services/BeaconService.cs index 5b622d45..9e1a4fcc 100644 --- a/LANCommander.SDK/Services/BeaconService.cs +++ b/LANCommander.SDK/Services/BeaconService.cs @@ -1,42 +1,29 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Net; using System.Net.NetworkInformation; -using System.Net.Sockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Helpers; using LANCommander.SDK.Interceptors; using LANCommander.SDK.Models; using Microsoft.Extensions.Logging; namespace LANCommander.SDK.Services; -public class BeaconService +public class BeaconService( + ILogger logger, + INetworkInformationProvider networkInformationProvider) { public delegate void OnBeaconResponseHandler(object sender, BeaconResponseArgs e); public event OnBeaconResponseHandler OnBeaconResponse; - private readonly Client _client; - private readonly ILogger _logger; - private List _probeClients = new(); private List _beaconClients = new(); private List _beaconMessageInterceptors = new(); - public BeaconService(Client client) - { - _client = client; - } - - public BeaconService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public void Initialize() { _beaconMessageInterceptors = new List(); @@ -61,7 +48,7 @@ public class BeaconService { int attempt = 0; - foreach (var networkInterface in GetNetworkInterfaces()) + foreach (var networkInterface in networkInformationProvider.GetNetworkInterfaces()) { DiscoveryProbe probeClient = null; try @@ -140,7 +127,7 @@ public class BeaconService string address, string name) { - foreach (var networkInterface in GetNetworkInterfaces()) + foreach (var networkInterface in networkInformationProvider.GetNetworkInterfaces()) { try { @@ -154,7 +141,7 @@ public class BeaconService { Address = address, Name = name, - Version = Client.GetCurrentVersion().ToString(), + Version = VersionHelper.GetCurrentVersion().ToString(), }; foreach (var interceptor in _beaconMessageInterceptors) @@ -169,12 +156,12 @@ public class BeaconService } catch (NetworkInformationException) { - _logger?.LogError("Unable to start beacon on network interface {NetworkInterface}", + logger?.LogError("Unable to start beacon on network interface {NetworkInterface}", networkInterface.Name); } catch (Exception ex) { - _logger?.LogError(ex, "Unknown error while starting beacon on network interface {NetworkInterface}", networkInterface.Name); + logger?.LogError(ex, "Unknown error while starting beacon on network interface {NetworkInterface}", networkInterface.Name); } } } @@ -189,47 +176,4 @@ public class BeaconService beaconClient.Dispose(); } } - - /// - /// Get active network interfaces on the system - /// - /// - private IEnumerable GetNetworkInterfaces() - { - var networkInterfaces = NetworkInterface - .GetAllNetworkInterfaces() - .Where(i => i.OperationalStatus == OperationalStatus.Up && - i.NetworkInterfaceType != NetworkInterfaceType.Loopback); - - return networkInterfaces; - } - - private IEnumerable GetBroadcastAddresses() - { - var networkInterfaces = GetNetworkInterfaces(); - - foreach (var nic in networkInterfaces) - { - foreach (var ua in nic.GetIPProperties().UnicastAddresses) - { - if (ua.Address.AddressFamily == AddressFamily.InterNetwork) - { - var ip = ua.Address; - var mask = ua.IPv4Mask; - - if (mask == null) - continue; - - var ipBytes = ip.GetAddressBytes(); - var maskBytes = mask.GetAddressBytes(); - var broadcastBytes = new byte[4]; - - for (var i = 0; i < 4; i++) - broadcastBytes[i] = (byte)(ipBytes[i] | (maskBytes[i] ^ 255)); - - yield return new IPAddress(broadcastBytes); - } - } - } - } } \ No newline at end of file diff --git a/LANCommander.SDK/Services/ChatService.cs b/LANCommander.SDK/Services/ChatService.cs index f703f144..9fd7d1ab 100644 --- a/LANCommander.SDK/Services/ChatService.cs +++ b/LANCommander.SDK/Services/ChatService.cs @@ -3,19 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using LANCommander.SDK.Models; +using LANCommander.SDK.Rpc.Client; namespace LANCommander.SDK.Services; -public class ChatService +public class ChatService(IRpcClient rpc) { - private readonly Client _client; private readonly Dictionary _threads = new(); - - public ChatService(Client client) - { - _client = client; - } - public ChatThread GetThread(Guid threadId) { return _threads[threadId]; @@ -23,7 +17,7 @@ public class ChatService public async Task StartThreadAsync(IEnumerable userIdentifiers) { - var threadId = await _client.RPC.Server.Chat_StartThreadAsync(userIdentifiers.ToArray()); + var threadId = await rpc.Server.Chat_StartThreadAsync(userIdentifiers.ToArray()); if (threadId != Guid.Empty) _threads[threadId] = new ChatThread @@ -41,7 +35,7 @@ public class ChatService public async Task> GetThreadsAsync() { - var threads = await _client.RPC.Server.Chat_GetThreadsAsync(); + var threads = await rpc.Server.Chat_GetThreadsAsync(); _threads.Clear(); @@ -77,11 +71,11 @@ public class ChatService public async Task GetMessagesAsync(Guid threadId) { - await _client.RPC.Server.Chat_GetMessagesAsync(threadId); + await rpc.Server.Chat_GetMessagesAsync(threadId); } public async Task SendMessageAsync(Guid threadId, string contents) { - await _client.RPC.Server.Chat_SendMessageAsync(threadId, contents); + await rpc.Server.Chat_SendMessageAsync(threadId, contents); } } \ No newline at end of file diff --git a/LANCommander.SDK/Services/ConnectionService.cs b/LANCommander.SDK/Services/ConnectionService.cs new file mode 100644 index 00000000..aedd922d --- /dev/null +++ b/LANCommander.SDK/Services/ConnectionService.cs @@ -0,0 +1,103 @@ +using System; +using System.Buffers.Text; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using LANCommander.SDK.Exceptions; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Factories; +using LANCommander.SDK.Rpc; +using LANCommander.SDK.Rpc.Client; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Services; + +public class ConnectionService( + ILogger logger, + IRpcClient rpc) : IConnectionService +{ + private Uri _serverAddress; + + public bool IsConnected() + { + return rpc.IsConnected(); + } + + public Uri GetServerAddress() => _serverAddress; + + public async Task UpdateServerAddressAsync(string address) + { + if (String.IsNullOrWhiteSpace(address)) + throw new InvalidAddressException("Server address cannot be blank"); + + var urisToTry = address.SuggestValidUris(); + + if (Uri.TryCreate(address, UriKind.RelativeOrAbsolute, out var baseUri)) + { + var hasPort = address.Replace(Uri.SchemeDelimiter, "").Contains(':'); + + if (hasPort) + urisToTry = urisToTry.Take(baseUri.IsAbsoluteUri ? 1 : 2); + } + + foreach (var uri in urisToTry) + { + logger?.LogInformation("Attempting to discover server at {ServerAddress}", uri.ToString()); + + try + { + if (await PingAsync()) + { + _serverAddress = uri; + + logger?.LogInformation("Successfully discovered server at {ServerAddress}", uri.ToString()); + + await rpc.ConnectAsync(); + + return; + } + } + catch + { + logger?.LogError("Failed to discover server at {ServerAddress}", uri.ToString()); + } + + throw new InvalidAddressException("Could not find a server at that address"); + } + } + + public async Task DisconnectAsync() + { + return await rpc.DisconnectAsync(); + } + + public async Task PingAsync(Uri serverAddress = null) + { + try + { + var pingId = Guid.NewGuid().ToString(); + + var pingHttpClient = new HttpClient(); + + pingHttpClient.BaseAddress = serverAddress ?? _serverAddress; + pingHttpClient.Timeout = TimeSpan.FromSeconds(1); + + var httpRequest = new HttpRequestMessage(); + + httpRequest.Headers.Add("X-Ping", pingId); + httpRequest.Method = HttpMethod.Head; + + var response = await pingHttpClient.SendAsync(httpRequest); + + return response.IsSuccessStatusCode + && + response.Headers.Contains("X-Pong") + && + response.Headers.GetValues("X-Pong").First() == pingId.FastReverse(); + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Services/DepotService.cs b/LANCommander.SDK/Services/DepotService.cs index da8cfbdd..fab03e39 100644 --- a/LANCommander.SDK/Services/DepotService.cs +++ b/LANCommander.SDK/Services/DepotService.cs @@ -3,43 +3,39 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; using LANCommander.SDK.Exceptions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class DepotService + public class DepotService( + ILogger logger, + ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - - private readonly Client _client; - - public DepotService(Client client) - { - _client = client; - } - - public DepotService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public async Task GetAsync() { - var results = await _client.GetRequestAsync("/api/Depot"); + var results = await apiRequestFactory.Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Depot") + .GetAsync(); if (results == null) { - _logger?.LogDebug("Could not find any depot results"); + logger?.LogDebug("Could not find any depot results"); throw new DepotNoResultsException("Did not find any depot results"); } - return results; } public async Task GetGameAsync(Guid gameId) { - return await _client.GetRequestAsync($"/api/Depot/Games/{gameId}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Depot/Games/{gameId}") + .GetAsync(); } } } diff --git a/LANCommander.SDK/Services/GameService.cs b/LANCommander.SDK/Services/GameService.cs index 3900d0d4..f0aa6ebf 100644 --- a/LANCommander.SDK/Services/GameService.cs +++ b/LANCommander.SDK/Services/GameService.cs @@ -14,6 +14,8 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { @@ -57,12 +59,19 @@ namespace LANCommander.SDK.Services public GameInstallationFileList FileList { get; set; } = GameInstallationFileList.Empty; } - public class GameService + public class GameService( + ILogger logger, + ApiRequestFactory apiRequestFactory, + ProcessExecutionContextFactory processExecutionContextFactory, + INetworkInformationProvider networkInformationProvider, + ILANCommanderConfiguration config, + IConnectionService connectionService, + RedistributableService redistributableService, + SaveService saveService, + ScriptService scriptService, + ProfileService profileService, + LobbyService lobbyService) { - private readonly ILogger _logger; - private readonly Client _client; - private string DefaultInstallDirectory { get; set; } - public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; @@ -82,37 +91,34 @@ namespace LANCommander.SDK.Services private readonly Dictionary _running = new(); - public GameService(Client client, string defaultInstallDirectory) - { - _client = client; - DefaultInstallDirectory = defaultInstallDirectory; - } - - public GameService(Client client, string defaultInstallDirectory, ILogger logger) - { - _client = client; - DefaultInstallDirectory = defaultInstallDirectory; - _logger = logger; - } - public async Task> GetAsync() { - return await _client.GetRequestAsync>("/api/Games"); - } - - public Game Get(Guid id) - { - return _client.GetRequest($"/api/Games/{id}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Games") + .GetAsync>(); } public async Task GetAsync(Guid id) { - return await _client.GetRequestAsync($"/api/Games/{id}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}") + .GetAsync(); } - public GameManifest GetManifest(Guid id) + public async Task GetManifestAsync(Guid id) { - return _client.GetRequest($"/api/Games/{id}/Manifest"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Manifest") + .GetAsync(); } public async Task> GetManifestsAsync(string installDirectory, Guid id) @@ -141,7 +147,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, $"Could not load manifest from dependent game {dependentGameId}"); + logger?.LogError(ex, $"Could not load manifest from dependent game {dependentGameId}"); } } } @@ -155,12 +161,21 @@ namespace LANCommander.SDK.Services try { - if (_client.IsConnected()) - actions.AddRange(await _client.GetRequestAsync>($"/api/Games/{id}/Actions")); + if (connectionService.IsConnected()) + { + actions.AddRange( + await apiRequestFactory + .Create() + .UseRoute($"/api/Games/{id}/Actions") + .UseAuthenticationToken() + .UseVersioning() + .GetAsync>() + ); + } } catch (Exception ex) { - _logger?.LogError(ex, "Could not get actions from server"); + logger?.LogError(ex, "Could not get actions from server"); } var manifests = await GetManifestsAsync(installDirectory, id); @@ -181,7 +196,7 @@ namespace LANCommander.SDK.Services try { - var lobbies = _client.Lobbies.GetSteamLobbies(installDirectory, id); + var lobbies = lobbyService.GetSteamLobbies(installDirectory, id); foreach (var lobby in lobbies) { @@ -201,7 +216,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not get lobbies"); + logger?.LogError(ex, "Could not get lobbies"); } } @@ -210,84 +225,91 @@ namespace LANCommander.SDK.Services public async Task> GetAddonsAsync(Guid id) { - return await _client.GetRequestAsync>($"/api/Games/{id}/Addons"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Addons") + .GetAsync>(); } public async Task CheckForUpdateAsync(Guid id, string currentVersion) { - return await _client.GetRequestAsync($"/api/Games/{id}/CheckForUpdate?version={currentVersion}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/CheckForUpdate?version={currentVersion}") + .GetAsync(); } - private TrackableStream Stream(Guid id) + private async Task StreamAsync(Guid id) { - return _client.StreamRequest($"/api/Games/{id}/Download"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Download") + .StreamAsync(); } public async Task StartedAsync(Guid id) { - _logger?.LogTrace("Signaling to the server that we started the game..."); + logger?.LogTrace("Signaling to the server that we started the game..."); try { - await _client.GetRequestAsync($"/api/Games/{id}/Started"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Started") + .GetAsync(); } catch (Exception ex) { - _logger?.LogError(ex, "Failed sending start request to server"); + logger?.LogError(ex, "Failed sending start request to server"); } } public async Task StoppedAsync(Guid id) { - _logger?.LogTrace("Signaling to the server that we stopped the game..."); + logger?.LogTrace("Signaling to the server that we stopped the game..."); try { - await _client.GetRequestAsync($"/api/Games/{id}/Stopped"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/{id}/Stopped") + .GetAsync(); } catch (Exception ex) { - _logger?.LogError(ex, "Failed sending stop request to server"); + logger?.LogError(ex, "Failed sending stop request to server"); } } - public string GetAllocatedKey(Guid id) - { - _logger?.LogTrace("Requesting allocated key..."); - - var macAddress = _client.GetMacAddress(); - - var request = new KeyRequest() - { - GameId = id, - MacAddress = macAddress, - ComputerName = Environment.MachineName, - IpAddress = _client.GetIpAddress(), - }; - - var response = _client.PostRequest($"/api/Keys/GetAllocated/{id}", request); - - if (response == null) - return string.Empty; - - return response.Value; - } - public async Task GetAllocatedKeyAsync(Guid id) { - _logger?.LogTrace("Requesting allocated key..."); - - var macAddress = _client.GetMacAddress(); - + logger?.LogTrace("Requesting allocated key..."); + var request = new KeyRequest() { GameId = id, - MacAddress = macAddress, + MacAddress = networkInformationProvider.GetMacAddress(), ComputerName = Environment.MachineName, - IpAddress = _client.GetIpAddress(), + IpAddress = networkInformationProvider.GetIpAddress(), }; - var response = await _client.PostRequestAsync($"/api/Keys/GetAllocated/{id}", request); + var response = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Keys/GetAllocated/{id}") + .AddBody(request) + .PostAsync(); if (response == null) return string.Empty; @@ -295,21 +317,25 @@ namespace LANCommander.SDK.Services return response.Value; } - public string GetNewKey(Guid id) + public async Task GetNewKey(Guid id) { - _logger?.LogTrace("Requesting new key allocation..."); - - var macAddress = _client.GetMacAddress(); + logger?.LogTrace("Requesting new key allocation..."); var request = new KeyRequest() { GameId = id, - MacAddress = macAddress, + MacAddress = networkInformationProvider.GetMacAddress(), ComputerName = Environment.MachineName, - IpAddress = _client.GetIpAddress(), + IpAddress = networkInformationProvider.GetIpAddress(), }; - var response = _client.PostRequest($"/api/Keys/Allocate/{id}", request); + var response = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Keys/Allocate/{id}") + .AddBody(request) + .PostAsync(); if (response == null) return string.Empty; @@ -337,9 +363,9 @@ namespace LANCommander.SDK.Services GameManifest manifest = null; if (string.IsNullOrWhiteSpace(installDirectory)) - installDirectory = _client.DefaultInstallDirectory; + installDirectory = config.InstallDirectories.First(); - var game = Get(gameId); + var game = await GetAsync(gameId); var destination = await GetInstallDirectory(game, installDirectory); _installProgress.Game = game; @@ -355,7 +381,7 @@ namespace LANCommander.SDK.Services // Handle Standalone Mods if (game.Type == GameType.StandaloneMod && game.BaseGameId != Guid.Empty) { - var baseGame = await _client.Games.GetAsync(game.BaseGameId); + var baseGame = await GetAsync(game.BaseGameId); destination = await GetInstallDirectory(baseGame, installDirectory); @@ -373,17 +399,17 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogTrace(ex, "Error reading manifest before install"); + logger?.LogTrace(ex, "Error reading manifest before install"); } - _logger?.LogTrace("Installing game {GameTitle} ({GameId})", game.Title, game.Id); + logger?.LogTrace("Installing game {GameTitle} ({GameId})", game.Title, game.Id); // Download and extract var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), async () => { - _logger?.LogTrace("Attempting to download and extract game"); + logger?.LogTrace("Attempting to download and extract game"); - return await Task.Run(() => DownloadAndExtract(game, destination)); + return await Task.Run(async () => await DownloadAndExtractAsync(game, destination)); }); if (!result.Success && !result.Canceled) @@ -397,7 +423,7 @@ namespace LANCommander.SDK.Services // Game is extracted, get metadata var writeManifestSuccess = await RetryHelper.RetryOnExceptionAsync(maxAttempts, TimeSpan.FromSeconds(1), false, async () => { - _logger?.LogTrace("Attempting to get game manifest"); + logger?.LogTrace("Attempting to get game manifest"); manifest = await WriteManifestAsync(game.InstallDirectory, game); return true; @@ -429,20 +455,20 @@ namespace LANCommander.SDK.Services #region Install Redistributables if (game.Redistributables != null && game.Redistributables.Any()) { - _logger?.LogTrace("Installing redistributables"); + logger?.LogTrace("Installing redistributables"); - await _client.Redistributables.InstallAsync(game); + await redistributableService.InstallAsync(game); } #endregion #region Download Latest Save - _logger?.LogTrace("Attempting to download the latest save"); + logger?.LogTrace("Attempting to download the latest save"); _installProgress.Status = InstallStatus.DownloadingSaves; OnInstallProgressUpdate?.Invoke(_installProgress); - await _client.Saves.DownloadAsync(game.InstallDirectory, game.Id); + await saveService.DownloadAsync(game.InstallDirectory, game.Id); #endregion await RunPostInstallScripts(game); @@ -464,7 +490,7 @@ namespace LANCommander.SDK.Services public async Task InstallAddonsAsync(string installDirectory, Guid baseGameId, IEnumerable addonIds) { - var game = await _client.Games.GetAsync(baseGameId); + var game = await GetAsync(baseGameId); return await InstallAddonsAsync(installDirectory, game, addonIds); } @@ -482,11 +508,11 @@ namespace LANCommander.SDK.Services { try { - addons.Add(await _client.Games.GetAsync(addonId)); + addons.Add(await GetAsync(addonId)); } catch (Exception ex) { - _logger?.LogError(ex, "Could not get information for addon with ID {AddonId}, skipping install", addonId); + logger?.LogError(ex, "Could not get information for addon with ID {AddonId}, skipping install", addonId); } } @@ -510,7 +536,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not install expansion with ID {AddonId}", expansion.Id); + logger?.LogError(ex, "Could not install expansion with ID {AddonId}", expansion.Id); } } @@ -534,7 +560,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not install mod with ID {AddonId}", mod.Id); + logger?.LogError(ex, "Could not install mod with ID {AddonId}", mod.Id); } } } @@ -559,7 +585,7 @@ namespace LANCommander.SDK.Services } catch (InstallCanceledException ex) { - _logger?.LogDebug("Install canceled"); + logger?.LogDebug("Install canceled"); _installProgress.Status = InstallStatus.Canceled; OnInstallProgressUpdate?.Invoke(_installProgress); @@ -568,7 +594,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Failed to install addon {AddonTitle} ({AddonId})", addon.Title, addon.Id); + logger?.LogError(ex, "Failed to install addon {AddonTitle} ({AddonId})", addon.Title, addon.Id); _installProgress.Status = InstallStatus.Failed; OnInstallProgressUpdate?.Invoke(_installProgress); @@ -588,7 +614,7 @@ namespace LANCommander.SDK.Services var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); if (manifest == null) { - _logger?.LogInformation("Unable to read or find manifest for game with ID {GameId}. Skip uninstallation!", gameId); + logger?.LogInformation("Unable to read or find manifest for game with ID {GameId}. Skip uninstallation!", gameId); return installResult; } @@ -611,7 +637,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogWarning("Could not uninstall dependent game with ID {GameId}. Assuming it's already uninstalled or never installed...", gameId); + logger?.LogWarning("Could not uninstall dependent game with ID {GameId}. Assuming it's already uninstalled or never installed...", gameId); } } } @@ -625,7 +651,7 @@ namespace LANCommander.SDK.Services var fileList = await File.ReadAllLinesAsync(fileListPath); var files = fileList.Select(l => l.Split('|').FirstOrDefault()?.Trim()); - _logger?.LogDebug("Attempting to delete the install files"); + logger?.LogDebug("Attempting to delete the install files"); foreach (var file in files.Where(f => f != null && !f.EndsWith("/"))) { @@ -641,22 +667,22 @@ namespace LANCommander.SDK.Services if (File.Exists(localPath)) File.Delete(localPath); - _logger?.LogTrace("Deleted file {LocalPath}", localPath); + logger?.LogTrace("Deleted file {LocalPath}", localPath); } catch (Exception ex) { - _logger?.LogWarning(ex, "Could not remove file {LocalPath}", localPath); + logger?.LogWarning(ex, "Could not remove file {LocalPath}", localPath); } } - _logger?.LogDebug("Attempting to delete any empty directories"); + logger?.LogDebug("Attempting to delete any empty directories"); DirectoryHelper.DeleteEmptyDirectories(installDirectory); if (!Directory.Exists(installDirectory)) - _logger?.LogDebug("Deleted install directory {InstallDirectory}", installDirectory); + logger?.LogDebug("Deleted install directory {InstallDirectory}", installDirectory); else - _logger?.LogTrace("Removed game files for {GameTitle} ({GameId})", manifest.Title, gameId); + logger?.LogTrace("Removed game files for {GameTitle} ({GameId})", manifest.Title, gameId); } else { @@ -664,7 +690,7 @@ namespace LANCommander.SDK.Services } #endregion - await _client.Scripts.RunUninstallScriptAsync(installDirectory, gameId); + await scriptService.RunUninstallScriptAsync(installDirectory, gameId); #region Cleanup Install Directory var metadataPath = GetMetadataDirectoryPath(installDirectory, gameId); @@ -686,7 +712,7 @@ namespace LANCommander.SDK.Services var baseManifest = await ManifestHelper.ReadAsync(installDirectory, baseGameId); if (baseManifest == null) { - _logger?.LogInformation("Unable to read or find manifest for addon game with ID {GameId}. Skip uninstallation!", baseGameId); + logger?.LogInformation("Unable to read or find manifest for addon game with ID {GameId}. Skip uninstallation!", baseGameId); return installResult; } @@ -707,7 +733,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogWarning(ex, $"Could not uninstall dependent game {dependentGame} of base game {baseGameId}. Assuming it's already uninstalled or never installed..."); + logger?.LogWarning(ex, $"Could not uninstall dependent game {dependentGame} of base game {baseGameId}. Assuming it's already uninstalled or never installed..."); } } @@ -752,7 +778,7 @@ namespace LANCommander.SDK.Services foreach (var dependentGameId in game.DependentGames) { - var dependentGame = await _client.Games.GetAsync(dependentGameId); + var dependentGame = await GetAsync(dependentGameId); if (dependentGame.IsAddon) gameAndAddons.Add(dependentGame); @@ -761,7 +787,7 @@ namespace LANCommander.SDK.Services foreach (var entry in gameAndAddons) { if (await IsInstalled(oldInstallDirectory, game, entry.Id)) - await _client.Saves.UploadAsync(oldInstallDirectory, entry.Id); + await saveService.UploadAsync(oldInstallDirectory, entry.Id); } if (Directory.Exists(newInstallDirectory)) @@ -834,7 +860,8 @@ namespace LANCommander.SDK.Services if (await IsInstalled(newInstallDirectory, game, entry.Id)) { await RunPostInstallScripts(entry); - await _client.Saves.DownloadAsync(newInstallDirectory, entry.Id); + + await saveService.DownloadAsync(newInstallDirectory, entry.Id); } } @@ -862,9 +889,9 @@ namespace LANCommander.SDK.Services private async Task WriteManifestAsync(string installDirectory, Game game) { - _logger?.LogTrace($"Retrieving game manifest for game {game.Title} with id {game.Id}"); - GameManifest manifest = GetManifest(game.Id); - _logger?.LogTrace($"Saving Manifest for game {game.Id} into {installDirectory}"); + logger?.LogTrace($"Retrieving game manifest for game {game.Title} with id {game.Id}"); + GameManifest manifest = await GetManifestAsync(game.Id); + logger?.LogTrace($"Saving Manifest for game {game.Id} into {installDirectory}"); await ManifestHelper.WriteAsync(manifest, installDirectory); return manifest; } @@ -873,7 +900,7 @@ namespace LANCommander.SDK.Services { if (game.Scripts != null) { - _logger?.LogTrace($"Saving scripts for game {game.Title} with id {game.Id} into {installDirectory}"); + logger?.LogTrace($"Saving scripts for game {game.Title} with id {game.Id} into {installDirectory}"); foreach (var script in game.Scripts) { @@ -894,27 +921,27 @@ namespace LANCommander.SDK.Services { var allocatedKey = await GetAllocatedKeyAsync(game.Id); - await _client.Scripts.RunInstallScriptAsync(game.InstallDirectory, game.Id); - await _client.Scripts.RunKeyChangeScriptAsync(game.InstallDirectory, game.Id, allocatedKey); - await _client.Scripts.RunNameChangeScriptAsync(game.InstallDirectory, game.Id, await _client.Profile.GetAliasAsync()); + await scriptService.RunInstallScriptAsync(game.InstallDirectory, game.Id); + await scriptService.RunKeyChangeScriptAsync(game.InstallDirectory, game.Id, allocatedKey); + await scriptService.RunNameChangeScriptAsync(game.InstallDirectory, game.Id, await profileService.GetAliasAsync()); } catch (Exception ex) { - _logger?.LogError(ex, "Scripts failed to execute for game/addon {GameTitle} ({GameId})", game.Title, game.Id); + logger?.LogError(ex, "Scripts failed to execute for game/addon {GameTitle} ({GameId})", game.Title, game.Id); } } } - private ExtractionResult DownloadAndExtract(Game game, string destination) + private async Task DownloadAndExtractAsync(Game game, string destination) { if (game == null) { - _logger?.LogTrace("Game failed to download, no game was specified"); + logger?.LogTrace("Game failed to download, no game was specified"); throw new ArgumentNullException("No game was specified"); } - _logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); + logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); var extractionResult = new ExtractionResult { @@ -928,7 +955,7 @@ namespace LANCommander.SDK.Services { Directory.CreateDirectory(destination); - _transferStream = Stream(game.Id); + _transferStream = await StreamAsync(game.Id); _reader = ReaderFactory.Open(_transferStream); using (var monitor = new FileTransferMonitor(_transferStream.Length)) @@ -1010,7 +1037,7 @@ namespace LANCommander.SDK.Services } catch { - _logger?.LogError("Could not skip to next entry in archive"); + logger?.LogError("Could not skip to next entry in archive"); } } catch (IOException ex) @@ -1020,7 +1047,7 @@ namespace LANCommander.SDK.Services if (errorCode == 87) throw ex; else - _logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message); + logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message); // Skip to next entry _reader.OpenEntryStream().Dispose(); @@ -1032,24 +1059,24 @@ namespace LANCommander.SDK.Services } catch (ReaderCancelledException ex) { - _logger?.LogTrace(ex, "User cancelled the download"); + logger?.LogTrace(ex, "User cancelled the download"); extractionResult.Canceled = true; if (Directory.Exists(destination)) { - _logger?.LogTrace("Cleaning up orphaned files after cancelled install"); + logger?.LogTrace("Cleaning up orphaned files after cancelled install"); Directory.Delete(destination, true); } } catch (Exception ex) { - _logger?.LogError(ex, "Could not extract to path {Destination}", destination); + logger?.LogError(ex, "Could not extract to path {Destination}", destination); if (Directory.Exists(destination)) { - _logger?.LogTrace("Cleaning up orphaned install files after bad install"); + logger?.LogTrace("Cleaning up orphaned install files after bad install"); Directory.Delete(destination, true); } @@ -1070,7 +1097,7 @@ namespace LANCommander.SDK.Services File.WriteAllText(fileListDestination, fileManifest.ToString()); - _logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); + logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); } return extractionResult; @@ -1079,7 +1106,7 @@ namespace LANCommander.SDK.Services public async Task GetInstallDirectory(Game game, string installDirectory) { if (string.IsNullOrWhiteSpace(installDirectory)) - installDirectory = _client.DefaultInstallDirectory; + installDirectory = config.InstallDirectories.First(); if ((game.Type == GameType.Expansion || game.Type == GameType.Mod || game.Type == GameType.StandaloneMod) && game.BaseGameId != Guid.Empty) { @@ -1091,7 +1118,7 @@ namespace LANCommander.SDK.Services } else { - var baseGame = await _client.Games.GetAsync(game.BaseGameId); + var baseGame = await GetAsync(game.BaseGameId); return await GetInstallDirectory(baseGame, installDirectory); } @@ -1128,7 +1155,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not load manifest from dependent game {DependentGameId}", dependentGameId); + logger?.LogError(ex, "Could not load manifest from dependent game {DependentGameId}", dependentGameId); } } } @@ -1150,7 +1177,13 @@ namespace LANCommander.SDK.Services /// protected async Task> GetGameInstallationArchiveEntries(Guid gameId, GameManifest manifest) { - var entries = await _client.GetRequestAsync>($"/api/Archives/Contents/{manifest.Id}/{manifest.Version}"); + var entries = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Archives/Contents/{manifest.Id}/{manifest.Version}") + .GetAsync>(); + return entries ?? []; } @@ -1179,7 +1212,7 @@ namespace LANCommander.SDK.Services gameArchives.BaseGame.Entries.AddRange(entries); manifests = manifests.Except([baseManifest]).ToList(); - var savePathEntries = baseManifest.SavePaths?.SelectMany(p => _client.Saves.GetFileSavePathEntries(p, installDirectory)).ToList() ?? []; + var savePathEntries = baseManifest.SavePaths?.SelectMany(p => saveService.GetFileSavePathEntries(p, installDirectory)).ToList() ?? []; gameArchives.BaseGame.SavePaths = savePathEntries; } @@ -1197,7 +1230,7 @@ namespace LANCommander.SDK.Services depArchiveInfo.Manifest = depManifest; depArchiveInfo.Entries.AddRange(depEntries); - var savePathEntries = depManifest.SavePaths?.SelectMany(p => _client.Saves.GetFileSavePathEntries(p, installDirectory)).ToList() ?? []; + var savePathEntries = depManifest.SavePaths?.SelectMany(p => saveService.GetFileSavePathEntries(p, installDirectory)).ToList() ?? []; depArchiveInfo.SavePaths = savePathEntries; } @@ -1208,9 +1241,9 @@ namespace LANCommander.SDK.Services { var screen = DisplayHelper.GetScreen(); - using (var context = new ProcessExecutionContext(_client, _logger)) + using (var context = processExecutionContextFactory.Create()) { - context.AddVariable("ServerAddress", _client.GetServerAddress()); + context.AddVariable("ServerAddress", connectionService.GetServerAddress().ToString()); try { @@ -1221,20 +1254,20 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not get display information for execution context variables"); + logger?.LogError(ex, "Could not get display information for execution context variables"); } try { - if (_client.IsConnected() && !String.IsNullOrWhiteSpace(_client.Settings.IPXRelayHost)) + if (connectionService.IsConnected() && !String.IsNullOrWhiteSpace(config.IPXRelayHost)) { - context.AddVariable("IPXRelayHost", await _client.GetIPXRelayHostAsync()); - context.AddVariable("IPXRelayPort", _client.Settings.IPXRelayPort.ToString()); + context.AddVariable("IPXRelayHost", config.IPXRelayHost); + context.AddVariable("IPXRelayPort", config.IPXRelayPort.ToString()); } } catch (Exception ex) { - _logger?.LogError(ex, "Could not connect to IPXRelay host"); + logger?.LogError(ex, "Could not connect to IPXRelay host"); } #region Run Scripts @@ -1247,19 +1280,19 @@ namespace LANCommander.SDK.Services var currentGameKey = await GetCurrentKeyAsync(installDirectory, manifest.Id); #region Check Game's Player Name - if (_client.IsConnected()) + if (connectionService.IsConnected()) { - var alias = await _client.Profile.GetAliasAsync(); + var alias = await profileService.GetAliasAsync(); if (currentGamePlayerAlias != alias) { - await _client.Scripts.RunNameChangeScriptAsync(installDirectory, gameId, alias); + await scriptService.RunNameChangeScriptAsync(installDirectory, gameId, alias); if (manifest.Redistributables != null) { foreach (var redistributable in manifest.Redistributables.Where(r => r.Scripts != null)) { - await _client.Scripts.RunNameChangeScriptAsync(installDirectory, gameId, redistributable.Id, alias); + await scriptService.RunNameChangeScriptAsync(installDirectory, gameId, redistributable.Id, alias); } } } @@ -1267,26 +1300,26 @@ namespace LANCommander.SDK.Services #endregion #region Check Key Allocation - if (_client.IsConnected()) + if (connectionService.IsConnected()) { - var newKey = await _client.Games.GetAllocatedKeyAsync(manifest.Id); + var newKey = await GetAllocatedKeyAsync(manifest.Id); if (currentGameKey != newKey) - await _client.Scripts.RunKeyChangeScriptAsync(installDirectory, manifest.Id, newKey); + await scriptService.RunKeyChangeScriptAsync(installDirectory, manifest.Id, newKey); } #endregion #region Download Latest Saves - if (_client.IsConnected()) + if (connectionService.IsConnected()) { await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromSeconds(1), false, async () => { - _logger?.LogTrace("Attempting to download save"); + logger?.LogTrace("Attempting to download save"); - var latestSave = await _client.Saves.GetLatestAsync(manifest.Id); + var latestSave = await saveService.GetLatestAsync(manifest.Id); if (latestSave != null && (latestSave.CreatedOn > lastRun || lastRun == null)) - await _client.Saves.DownloadAsync(installDirectory, manifest.Id); + await saveService.DownloadAsync(installDirectory, manifest.Id); return true; }); @@ -1294,13 +1327,13 @@ namespace LANCommander.SDK.Services #endregion #region Run Before Start Script - await _client.Scripts.RunBeforeStartScriptAsync(installDirectory, manifest.Id); + await scriptService.RunBeforeStartScriptAsync(installDirectory, manifest.Id); if (manifest.Redistributables != null) { foreach (var redistributable in manifest.Redistributables.Where(r => r.Scripts != null)) { - await _client.Scripts.RunBeforeStartScriptAsync(installDirectory, gameId, redistributable.Id); + await scriptService.RunBeforeStartScriptAsync(installDirectory, gameId, redistributable.Id); } } #endregion @@ -1322,19 +1355,19 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Game failed to run"); + logger?.LogError(ex, "Game failed to run"); } foreach (var manifest in manifests) { #region Run After Stop Script - await _client.Scripts.RunAfterStopScriptAsync(installDirectory, gameId); + await scriptService.RunAfterStopScriptAsync(installDirectory, gameId); if (manifest.Redistributables != null) { foreach (var redistributable in manifest.Redistributables.Where(r => r.Scripts != null)) { - await _client.Scripts.RunAfterStopScriptAsync(installDirectory, gameId, redistributable.Id); + await scriptService.RunAfterStopScriptAsync(installDirectory, gameId, redistributable.Id); } } #endregion @@ -1344,15 +1377,15 @@ namespace LANCommander.SDK.Services private async Task UploadSavesAsync(ICollection manifests, string installDirectory) { - if (_client.IsConnected()) + if (connectionService.IsConnected()) { foreach (var manifest in manifests) { await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromSeconds(1), false, async () => { - _logger?.LogTrace("Attempting to upload save"); + logger?.LogTrace("Attempting to upload save"); - await _client.Saves.UploadAsync(installDirectory, manifest.Id); + await saveService.UploadAsync(installDirectory, manifest.Id); return true; }); @@ -1382,32 +1415,57 @@ namespace LANCommander.SDK.Services { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await _client.ChunkedUploadRequestAsync("", fs); + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); if (objectKey != Guid.Empty) - await _client.PostRequestAsync($"/api/Games/Import/{objectKey}"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/Import/{objectKey}") + .PostAsync(); } } + [Obsolete("Servers no longer do \"Full\" exports")] public async Task ExportAsync(string destinationPath, Guid gameId) { - await _client.DownloadRequestAsync($"/Games/{gameId}/Export/Full", destinationPath); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Games/Export/Full") + .DownloadAsync(destinationPath); } public async Task UploadArchiveAsync(string archivePath, Guid gameId, string version, string changelog = "") { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await _client.ChunkedUploadRequestAsync("", fs); + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); if (objectKey != Guid.Empty) - await _client.PostRequestAsync($"/api/Games/UploadArchive", new UploadArchiveRequest - { - Id = gameId, - ObjectKey = objectKey, - Version = version, - Changelog = changelog, - }); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Games/UploadArchive") + .AddBody(new UploadArchiveRequest + { + Id = gameId, + ObjectKey = objectKey, + Version = version, + Changelog = changelog + }) + .PostAsync(); } } @@ -1535,13 +1593,12 @@ namespace LANCommander.SDK.Services public async Task DownloadFilesAsync(string installDirectory, Guid gameId, ICollection entries) { var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - var archive = await _client.GetRequestAsync($"/api/Archives/ByVersion/{manifest.Version}"); - await Task.Run(() => + await Task.Run(async () => { try { - _transferStream = Stream(gameId); + _transferStream = await StreamAsync(gameId); _reader = ReaderFactory.Open(_transferStream); while (_reader.MoveToNextEntry()) @@ -1568,7 +1625,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not skip to the next entry in the archive"); + logger?.LogError(ex, "Could not skip to the next entry in the archive"); } } catch (IOException ex) @@ -1578,7 +1635,7 @@ namespace LANCommander.SDK.Services if (errorCode == 87) throw; else - _logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message); + logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message); // Skip to next entry _reader.OpenEntryStream().Dispose(); diff --git a/LANCommander.SDK/Services/IConnectionService.cs b/LANCommander.SDK/Services/IConnectionService.cs new file mode 100644 index 00000000..30a15a76 --- /dev/null +++ b/LANCommander.SDK/Services/IConnectionService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Services; + +public interface IConnectionService +{ + public bool IsConnected(); + public Uri GetServerAddress(); + + /// + /// Set the server address to use for all API requests. Address is validated and checked for validity. + /// + /// The address to resolve for a LANCommander server + public Task UpdateServerAddressAsync(string address); + + public Task DisconnectAsync(); +} \ No newline at end of file diff --git a/LANCommander.SDK/Services/IssueService.cs b/LANCommander.SDK/Services/IssueService.cs index 8d8c8028..cd5d6dac 100644 --- a/LANCommander.SDK/Services/IssueService.cs +++ b/LANCommander.SDK/Services/IssueService.cs @@ -1,52 +1,26 @@ -using Force.Crc32; -using LANCommander.SDK.Extensions; -using LANCommander.SDK.Helpers; -using LANCommander.SDK.Models; -using LANCommander.SDK.PowerShell; +using LANCommander.SDK.Models; using Microsoft.Extensions.Logging; -using RestSharp; -using SharpCompress.Archives; -using SharpCompress.Archives.Zip; -using SharpCompress.Common; -using SharpCompress.Readers; using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class IssueService + public class IssueService(ApiRequestFactory apiRequestFactory) { - private readonly ILogger Logger; - - private readonly Client Client; - - public IssueService(Client client) - { - Client = client; - } - - public IssueService(Client client, ILogger logger) - { - Client = client; - Logger = logger; - } - public async Task Open(string description, Guid gameId) { - return await Client.PostRequestAsync("/api/Issue/Open", new Issue - { - Description = description, - GameId = gameId - }); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Issue/Open") + .AddBody(new Issue + { + Description = description, + GameId = gameId, + }) + .PostAsync(); } } } diff --git a/LANCommander.SDK/Services/LauncherService.cs b/LANCommander.SDK/Services/LauncherService.cs index a9d7b9ca..7461fc31 100644 --- a/LANCommander.SDK/Services/LauncherService.cs +++ b/LANCommander.SDK/Services/LauncherService.cs @@ -1,38 +1,27 @@ using LANCommander.SDK.Models; using Microsoft.Extensions.Logging; -using RestSharp; using System; using System.Threading.Tasks; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class LauncherService + public class LauncherService( + ILogger logger, + ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - private readonly Client _client; - - public LauncherService(Client client) - { - _client = client; - } - - public LauncherService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public async Task CheckForUpdateAsync() { try { - var request = new RestRequest("/api/Launcher", Method.Get); - - return await _client.GetRequestAsync("/api/Launcher/CheckForUpdate", true); + return await apiRequestFactory + .Create() + .UseRoute("/api/Launcher/CheckForUpdate") + .GetAsync(); } catch (Exception ex) { - _logger?.LogError(ex, "Could not check for updates from server"); + logger?.LogError(ex, "Could not check for updates from server"); } return null; @@ -40,9 +29,14 @@ namespace LANCommander.SDK.Services public async Task DownloadAsync(string destination) { - _logger?.LogTrace("Downloading the launcher"); + logger?.LogTrace("Downloading the launcher"); - return await _client.DownloadRequestAsync("/api/Launcher/Download", destination); + var result = await apiRequestFactory + .Create() + .UseRoute("/api/Launcher/Download") + .DownloadAsync(destination); + + return result.FullName; } } } diff --git a/LANCommander.SDK/Services/LibraryService.cs b/LANCommander.SDK/Services/LibraryService.cs index 1b64fa4e..36cceb0b 100644 --- a/LANCommander.SDK/Services/LibraryService.cs +++ b/LANCommander.SDK/Services/LibraryService.cs @@ -1,49 +1,57 @@ using LANCommander.SDK.Models; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class LibraryService + public class LibraryService(ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - private readonly Client _client; - - public LibraryService(Client client) - { - _client = client; - } - - public LibraryService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public async Task> GetAsync() { - var results = await _client.GetRequestAsync>("/api/Library"); + var results = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Library") + .GetAsync>(); return results ?? []; } public async Task AddToLibrary(Guid gameId) { - return await _client.PostRequestAsync($"/api/Library/AddToLibrary/{gameId}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Library/AddToLibrary/{gameId}") + .PostAsync(); } public async Task RemoveFromLibrary(Guid gameId) { - return await _client.PostRequestAsync($"/api/Library/RemoveFromLibrary/{gameId}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Library/RemoveFromLibrary/{gameId}") + .PostAsync(); } public async Task RemoveFromLibrary(Guid gameId, Guid[] addonIds) { - var requestBody = new GenericGuidsRequest { Guids = addonIds }; - return await _client.PostRequestAsync($"/api/Library/RemoveFromLibrary/{gameId}/addons", requestBody); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Library/RemoveFromLibrary/{gameId}/addons") + .AddBody(new GenericGuidsRequest + { + Guids = addonIds + }) + .PostAsync(); } } } diff --git a/LANCommander.SDK/Services/LobbyService.cs b/LANCommander.SDK/Services/LobbyService.cs index 4db7546a..dac7079e 100644 --- a/LANCommander.SDK/Services/LobbyService.cs +++ b/LANCommander.SDK/Services/LobbyService.cs @@ -7,22 +7,8 @@ using System.IO; namespace LANCommander.SDK.Services { - public class LobbyService + public class LobbyService(ILogger logger) { - private readonly ILogger _logger; - private readonly Client _client; - - public LobbyService(Client client) - { - _client = client; - } - - public LobbyService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - /// /// Get all Steam lobbies for a specified game. Game install directory must contain a file called steam_appid.txt. /// Remember to call ReleaseSteam() when the game is done playing or lobby join is canceled! @@ -45,7 +31,7 @@ namespace LANCommander.SDK.Services try { - _logger?.LogTrace("Initializing Steamworks with app ID {AppId}", appId); + logger?.LogTrace("Initializing Steamworks with app ID {AppId}", appId); SteamClient.Init(appId, true); @@ -64,13 +50,13 @@ namespace LANCommander.SDK.Services lobbies.Add(lobby); - _logger?.LogTrace("Found lobby | {FriendName} ({FriendId}): {LobbyId}", lobby.ExternalUsername, lobby.ExternalUserId, lobby.Id); + logger?.LogTrace("Found lobby | {FriendName} ({FriendId}): {LobbyId}", lobby.ExternalUsername, lobby.ExternalUserId, lobby.Id); } } } catch (Exception ex) { - _logger?.LogError(ex, "Couldn't initialize Steamworks"); + logger?.LogError(ex, "Couldn't initialize Steamworks"); } return lobbies; diff --git a/LANCommander.SDK/Services/MediaService.cs b/LANCommander.SDK/Services/MediaService.cs index 54e107dc..59fc9097 100644 --- a/LANCommander.SDK/Services/MediaService.cs +++ b/LANCommander.SDK/Services/MediaService.cs @@ -1,42 +1,40 @@ using Force.Crc32; using LANCommander.SDK.Models; -using Microsoft.Extensions.Logging; using System; using System.IO; using System.Threading.Tasks; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class MediaService + public class MediaService( + ApiRequestFactory apiRequestFactory, + IConnectionService connectionService) { - private readonly ILogger _logger; - - private readonly Client _client; - - public MediaService(Client client) + public async Task GetAsync(Guid mediaId) { - _client = client; + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Media/{mediaId}") + .GetAsync(); } - public MediaService(Client client, ILogger logger) + public async Task DownloadAsync(Media media, string destination) { - _client = client; - _logger = logger; - } - - public async Task Get(Guid mediaId) - { - return await _client.GetRequestAsync($"/api/Media/{mediaId}"); - } - - public async Task DownloadAsync(Media media, string destination) - { - return await _client.DownloadRequestAsync(GetDownloadPath(media), destination); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute(GetDownloadPath(media)) + .DownloadAsync(destination); } public string GetAbsoluteUrl(Media media) { - return new Uri(_client.BaseUrl, GetDownloadPath(media)).ToString(); + return connectionService.GetServerAddress().Join(GetDownloadPath(media)).ToString(); } public string GetDownloadPath(Media media) @@ -46,7 +44,7 @@ namespace LANCommander.SDK.Services public string GetAbsoluteThumbnailUrl(Media media) { - return new Uri(_client.BaseUrl, GetThumbnailPath(media)).ToString(); + return connectionService.GetServerAddress().Join(GetThumbnailPath(media)).ToString(); } public string GetThumbnailPath(Media media) @@ -54,7 +52,7 @@ namespace LANCommander.SDK.Services return $"/api/Media/{media.Id}/Thumbnail"; } - public static string CalculateChecksum(string path) + public static async Task CalculateChecksumAsync(string path) { uint crc = 0; @@ -64,7 +62,7 @@ namespace LANCommander.SDK.Services while (true) { - var count = fs.Read(buffer, 0, buffer.Length); + var count = await fs.ReadAsync(buffer, 0, buffer.Length); if (count == 0) break; diff --git a/LANCommander.SDK/Services/PlaySessionService.cs b/LANCommander.SDK/Services/PlaySessionService.cs index 574bddfb..6b747d13 100644 --- a/LANCommander.SDK/Services/PlaySessionService.cs +++ b/LANCommander.SDK/Services/PlaySessionService.cs @@ -3,33 +3,30 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading.Tasks; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class PlaySessionService + public class PlaySessionService(ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - private readonly Client _client; - - public PlaySessionService(Client client) - { - _client = client; - } - - public PlaySessionService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public async Task> GetAsync() { - return await _client.GetRequestAsync>("/api/PlaySessions"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/PlaySessions") + .GetAsync>(); } public async Task> GetAsync(Guid gameId) { - return await _client.PostRequestAsync>($"/api/PlaySessions/{gameId}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/PlaySessions/{gameId}") + .GetAsync>(); } } } diff --git a/LANCommander.SDK/Services/ProfileService.cs b/LANCommander.SDK/Services/ProfileService.cs index e60f3cf3..4ac942e5 100644 --- a/LANCommander.SDK/Services/ProfileService.cs +++ b/LANCommander.SDK/Services/ProfileService.cs @@ -3,43 +3,27 @@ using Microsoft.Extensions.Logging; using System; using System.IO; using System.Threading.Tasks; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class ProfileService + public class ProfileService(ApiRequestFactory apiRequestFactory, ILogger logger) { - private readonly ILogger _logger; - private readonly Client _client; - private User _user; - - public ProfileService(Client client) - { - _client = client; - } - - public ProfileService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - - public User Get(bool forceLoad = false) - { - _logger?.LogTrace("Requesting player's profile..."); - - if (_user == null || forceLoad) - _user = _client.GetRequest("/api/Profile"); - - return _user; - } - + public async Task GetAsync(bool forceLoad = false) { - _logger?.LogTrace("Requesting player's profile..."); + logger?.LogTrace("Requesting player's profile..."); if (_user == null || forceLoad) - _user = await _client.GetRequestAsync("/api/Profile"); + { + _user = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Profile") + .GetAsync(); + } return _user; } @@ -52,7 +36,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not get user alias from server"); + logger?.LogError(ex, "Could not get user alias from server"); } return String.IsNullOrWhiteSpace(_user.Alias) ? _user.UserName : _user.Alias; @@ -60,28 +44,37 @@ namespace LANCommander.SDK.Services public async Task ChangeAliasAsync(string alias) { - _logger?.LogTrace("Requesting to change player alias..."); + logger?.LogTrace("Requesting to change player alias..."); if (_user == null) _user = new User(); _user.Alias = alias; - var response = await _client.PutRequestAsync("/api/Profile/ChangeAlias", new - { - Alias = alias - }); - - return response; + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Profile/ChangeAlias") + .AddBody(new + { + Alias = alias + }) + .PutAsync(); } public async Task GetAvatarAsync() { - _logger?.LogTrace("Requesting avatar contents..."); + logger?.LogTrace("Requesting avatar contents..."); using (var ms = new MemoryStream()) { - var stream = _client.StreamRequest("/api/Profile/Avatar"); + var stream = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Profile/Avatar") + .StreamAsync(); await stream.CopyToAsync(ms); @@ -91,25 +84,43 @@ namespace LANCommander.SDK.Services public async Task DownloadAvatar() { - _logger?.LogTrace("Retrieving player's avatar..."); + logger?.LogTrace("Retrieving player's avatar..."); var tempFile = Path.GetTempFileName(); - return await _client.DownloadRequestAsync("/api/Profile/Avatar", tempFile); + var result = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Profile/Avatar") + .DownloadAsync(tempFile); + + return result.FullName; } - public async Task GetCustomField(string name) + public async Task GetCustomFieldAsync(string name) { - _logger?.LogTrace("Getting player custom field with name {CustomFieldName}...", name); + logger?.LogTrace("Getting player custom field with name {CustomFieldName}...", name); - return await _client.GetRequestAsync($"/api/Profile/CustomField/{name}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Profile/CustomField/{name}") + .GetAsync(); } - public async Task UpdateCustomField(string name, string value) + public async Task UpdateCustomFieldAsync(string name, string value) { - _logger?.LogTrace("Updating player custom fields: {CustomFieldName} = {CustomFieldValue}", name, value); + logger?.LogTrace("Updating player custom fields: {CustomFieldName} = {CustomFieldValue}", name, value); - return await _client.PutRequestAsync($"/api/Profile/CustomField/{name}", value); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Profile/CustomField/{name}") + .AddBody(value) + .PutAsync(); } } } diff --git a/LANCommander.SDK/Services/RedistributableService.cs b/LANCommander.SDK/Services/RedistributableService.cs index e89b3cdf..6bcb0061 100644 --- a/LANCommander.SDK/Services/RedistributableService.cs +++ b/LANCommander.SDK/Services/RedistributableService.cs @@ -9,15 +9,19 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; using LANCommander.SDK.Exceptions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class RedistributableService + public class RedistributableService( + ILogger _logger, + ILANCommanderConfiguration config, + ApiRequestFactory apiRequestFactory, + ScriptService scriptService, + ProfileService profileService) { - private readonly ILogger _logger; - private readonly Client _client; - public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; @@ -28,21 +32,15 @@ namespace LANCommander.SDK.Services public event OnInstallProgressUpdateHandler OnInstallProgressUpdate; private InstallProgress _installProgress; - - public RedistributableService(Client client) + + public async Task Stream(Guid id) { - _client = client; - } - - public RedistributableService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - - public TrackableStream Stream(Guid id) - { - return _client.StreamRequest($"/api/Redistributables/{id}/Download"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Redistributable/{id}/Download") + .StreamAsync(); } public async Task InstallAsync(Game game) @@ -80,8 +78,9 @@ namespace LANCommander.SDK.Services { await ScriptHelper.SaveScriptAsync(game, redistributable, script.Type); } - - var installed = await _client.Scripts.RunDetectInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id); + + var installed = + await scriptService.RunDetectInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id); _logger?.LogTrace("Redistributable install detection returned {Result}", installed); @@ -99,7 +98,7 @@ namespace LANCommander.SDK.Services { _logger?.LogTrace("Attempting to download and extract redistributable"); - return await Task.Run(() => DownloadAndExtract(redistributable, game)); + return await Task.Run(async () => await DownloadAndExtractAsync(redistributable, game)); }); if (!result.Success && !result.Canceled) @@ -143,8 +142,8 @@ namespace LANCommander.SDK.Services try { - await _client.Scripts.RunInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id); - await _client.Scripts.RunNameChangeScriptAsync(game.InstallDirectory, game.Id, redistributable.Id, await _client.Profile.GetAliasAsync()); + await scriptService.RunInstallScriptAsync(game.InstallDirectory, game.Id, redistributable.Id); + await scriptService.RunNameChangeScriptAsync(game.InstallDirectory, game.Id, redistributable.Id, await profileService.GetAliasAsync()); } catch (Exception ex) { @@ -153,7 +152,7 @@ namespace LANCommander.SDK.Services } } - private ExtractionResult DownloadAndExtract(Redistributable redistributable, Game game) + private async Task DownloadAndExtractAsync(Redistributable redistributable, Game game) { if (redistributable == null) { @@ -170,11 +169,10 @@ namespace LANCommander.SDK.Services { Directory.CreateDirectory(destination); - using (var redistributableStream = Stream(redistributable.Id)) + using (var redistributableStream = await Stream(redistributable.Id)) using (var reader = ReaderFactory.Open(redistributableStream)) using (var monitor = new FileTransferMonitor(redistributableStream.Length)) { - redistributableStream.OnProgress += (pos, len) => { if (monitor.CanUpdate()) @@ -246,32 +244,57 @@ namespace LANCommander.SDK.Services { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await _client.ChunkedUploadRequestAsync("", fs); + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); if (objectKey != Guid.Empty) - await _client.PostRequestAsync($"/api/Redistributables/Import/{objectKey}"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Redistributables/Import/{objectKey}") + .PostAsync(); } } + [Obsolete("Exporter no longer provides \"full\" exports")] public async Task ExportAsync(string destinationPath, Guid redistributableId) { - await _client.DownloadRequestAsync($"/Redistributables/{redistributableId}/Export/Full", destinationPath); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/Redistributables/{redistributableId}/Export/Full") + .DownloadAsync(destinationPath); } public async Task UploadArchiveAsync(string archivePath, Guid redistributableId, string version, string changelog = "") { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await _client.ChunkedUploadRequestAsync("", fs); + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); if (objectKey != Guid.Empty) - await _client.PostRequestAsync($"/api/Redistributables/UploadArchive", new UploadArchiveRequest - { - Id = redistributableId, - ObjectKey = objectKey, - Version = version, - Changelog = changelog, - }); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Redistributables/UploadArchive") + .AddBody(new UploadArchiveRequest + { + Id = redistributableId, + ObjectKey = objectKey, + Version = version, + Changelog = changelog, + }) + .PostAsync(); } } } diff --git a/LANCommander.SDK/Services/SaveService.cs b/LANCommander.SDK/Services/SaveService.cs index 658e2bdb..b51693a1 100644 --- a/LANCommander.SDK/Services/SaveService.cs +++ b/LANCommander.SDK/Services/SaveService.cs @@ -13,7 +13,10 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Factories; using LANCommander.SDK.Utilities; +using Action = System.Action; // Some terms for this file since they're probably going to be needed in the future: // Local path - The full path of the file/directory on the local disk. No variables used, just the raw path for current machine @@ -25,57 +28,63 @@ using LANCommander.SDK.Utilities; namespace LANCommander.SDK.Services { - public class SaveService + public class SaveService( + ApiRequestFactory apiRequestFactory, + ILANCommanderConfiguration config, + ILogger logger) { - private readonly ILogger _logger; - - private readonly Client _client; - public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e); public event OnDownloadProgressHandler OnDownloadProgress; - public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e); + public delegate void OnDownloadCompleteHandler(); public event OnDownloadCompleteHandler OnDownloadComplete; - public SaveService(Client client) + private async Task DownloadAsync(Guid id, Action progressHandler, Action completeHandler) { - _client = client; + var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/{id}/Download") + .OnProgress(progressHandler) + .OnComplete(completeHandler) + .DownloadAsync(destination); } - public SaveService(Client client, ILogger logger) + public async Task DownloadLatestAsync(Guid gameId, Action progressHandler, Action completeHandler) { - _client = client; - _logger = logger; - } - - private async Task DownloadAsync(Guid id, Action progressHandler, Action completeHandler) - { - return await _client.DownloadRequestAsync($"/api/Saves/{id}/Download", progressHandler, completeHandler); - } - - public async Task DownloadLatestAsync(Guid gameId, Action progressHandler, Action completeHandler) - { - return await _client.DownloadRequestAsync($"/api/Saves/Game/{gameId}/Latest/Download", progressHandler, completeHandler); - } - - public IEnumerable Get(Guid gameId) - { - return _client.GetRequest>($"/api/Saves/Game/{gameId}"); + var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/Game/{gameId}/Latest/Download") + .OnProgress(progressHandler) + .OnComplete(completeHandler) + .DownloadAsync(destination); } public async Task> GetAsync(Guid gameId) { - return await _client.GetRequestAsync>($"/api/Saves/Game/{gameId}"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/Game/{gameId}") + .GetAsync>(); } - public GameSave GetLatest(Guid gameId) + public async Task GetLatestAsync(Guid gameId) { - return _client.GetRequest($"/api/Saves/Game/{gameId}/Latest"); - } - - public Task GetLatestAsync(Guid gameId) - { - return _client.GetRequestAsync($"/api/Saves/Game/{gameId}/Latest"); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/Game/{gameId}/Latest") + .GetAsync(); } public async Task DownloadAsync(string installDirectory, Guid gameId, Guid? saveId = null) @@ -87,16 +96,16 @@ namespace LANCommander.SDK.Services if (manifest != null) { - string destination; + FileInfo destination; if (!saveId.HasValue) { destination = await DownloadLatestAsync(manifest.Id, (changed) => { OnDownloadProgress?.Invoke(changed); - }, (complete) => + }, () => { - OnDownloadComplete?.Invoke(complete); + OnDownloadComplete?.Invoke(); }); } else @@ -104,19 +113,18 @@ namespace LANCommander.SDK.Services destination = await DownloadAsync(saveId.Value, (changed) => { OnDownloadProgress?.Invoke(changed); - }, (complete) => + }, () => { - OnDownloadComplete?.Invoke(complete); + OnDownloadComplete?.Invoke(); }); } - - - if (string.IsNullOrWhiteSpace(destination)) + + if (!destination.Exists) return; - _logger?.LogTrace("Game save archive downloaded to {SaveTempLocation}", destination); + logger?.LogTrace("Game save archive downloaded to {SaveTempLocation}", destination); - tempFile = destination; + tempFile = destination.FullName; // Go into the archive and extract the files to the correct locations try @@ -127,7 +135,7 @@ namespace LANCommander.SDK.Services bool success = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(200), false, () => { - _logger?.LogTrace("Attempting to extracting save entries to the temporary location {TempPath}", tempLocation); + logger?.LogTrace("Attempting to extracting save entries to the temporary location {TempPath}", tempLocation); ExtractFilesFromZip(tempFile, tempLocation); @@ -148,7 +156,7 @@ namespace LANCommander.SDK.Services foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == Enums.SavePathType.File)) { - var entries = _client.Saves.GetFileSavePathEntries(savePath, installDirectory) ?? []; + var entries = GetFileSavePathEntries(savePath, installDirectory) ?? []; foreach (var entry in entries) { @@ -209,12 +217,12 @@ namespace LANCommander.SDK.Services script.UseInline($"Start-Process regedit.exe {adminArgument} -ArgumentList \"/s\", \"{registryImportFilePath}\""); - if (_client.Scripts.Debug) + if (config.DebugScripts) { script.EnableDebug(); - script.DebugHandler.OnDebugStart = _client.Scripts.OnDebugStart; + /*script.DebugHandler.OnDebugStart = _client.Scripts.OnDebugStart; script.DebugHandler.OnDebugBreak = _client.Scripts.OnDebugBreak; - script.DebugHandler.OnOutput = _client.Scripts.OnOutput; + script.DebugHandler.OnOutput = _client.Scripts.OnOutput;*/ } await script.ExecuteAsync(); @@ -226,7 +234,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "The files in a save could not be extracted to their destination"); + logger?.LogError(ex, "The files in a save could not be extracted to their destination"); } finally { @@ -251,7 +259,12 @@ namespace LANCommander.SDK.Services public async Task UploadAsync(Stream stream, GameManifest manifest) { - return await _client.UploadRequestAsync($"/api/Saves/Game/{manifest.Id}/Upload", stream); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/{manifest.Id}/Upload") + .UploadAsync("", stream); } public async Task UploadAsync(string installDirectory, Guid gameId) @@ -276,7 +289,12 @@ namespace LANCommander.SDK.Services public async Task DeleteAsync(Guid id) { - await _client.DeleteRequestAsync($"/api/Saves/{id}"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Saves/{id}") + .DeleteAsync(); } public IEnumerable GetFileSavePathEntries(SavePath savePath, string installDirectory) diff --git a/LANCommander.SDK/Services/ScriptService.cs b/LANCommander.SDK/Services/ScriptService.cs index 5f986973..97eb9a95 100644 --- a/LANCommander.SDK/Services/ScriptService.cs +++ b/LANCommander.SDK/Services/ScriptService.cs @@ -8,15 +8,15 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; namespace LANCommander.SDK.Services { - public class ScriptService + public class ScriptService( + ILogger logger, + ILANCommanderConfiguration config, + IConnectionService connectionService) { - private readonly ILogger _logger; - - private readonly Client _client; - public delegate Task ExternalScriptRunnerHandler(PowerShellScript script); public event ExternalScriptRunnerHandler ExternalScriptRunner; @@ -25,24 +25,13 @@ namespace LANCommander.SDK.Services public Func OnDebugStart; public Func OnDebugBreak; public Func OnOutput; - - public ScriptService(Client client) - { - _client = client; - } - - public ScriptService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } #region Authentication Scripts public async Task RunUserLoginScript(Script loginScript, User user) { try { - using (var op = _logger.BeginOperation("Executing user login script")) + using (var op = logger.BeginOperation("Executing user login script")) { var script = new PowerShellScript(Enums.ScriptType.UserLogin); @@ -60,7 +49,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } await script.ExecuteAsync(); @@ -68,7 +57,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not execute user login script"); + logger?.LogError(ex, "Could not execute user login script"); } } @@ -76,7 +65,7 @@ namespace LANCommander.SDK.Services { try { - using (var op = _logger.BeginOperation("Executing user registration script")) + using (var op = logger.BeginOperation("Executing user registration script")) { var script = new PowerShellScript(Enums.ScriptType.UserRegistration); @@ -94,7 +83,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } await script.ExecuteAsync(); @@ -102,7 +91,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not execute user registration script"); + logger?.LogError(ex, "Could not execute user registration script"); } } #endregion @@ -121,7 +110,7 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - using (var op = _logger.BeginOperation("Executing install detection script")) + using (var op = logger.BeginOperation("Executing install detection script")) { var script = new PowerShellScript(Enums.ScriptType.DetectInstall); @@ -131,8 +120,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); script.AddVariable("RedistributableManifest", redistributableManifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); try { @@ -147,7 +136,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) @@ -172,7 +161,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -202,7 +191,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Detect Install script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Detect Install script"); } return result; @@ -221,7 +210,7 @@ namespace LANCommander.SDK.Services { if (Path.Exists(path)) { - using (var op = _logger.BeginOperation("Executing install detection script")) + using (var op = logger.BeginOperation("Executing install detection script")) { var script = new PowerShellScript(Enums.ScriptType.Install); @@ -231,8 +220,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); script.AddVariable("RedistributableManifest", redistributableManifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); try { @@ -248,7 +237,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) @@ -275,7 +264,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -292,7 +281,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Detect Install script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Detect Install script"); } return result; @@ -309,7 +298,7 @@ namespace LANCommander.SDK.Services var path = ScriptHelper.GetScriptFilePath(installDirectory, redistributableId, Enums.ScriptType.BeforeStart); - using (var op = _logger.BeginOperation("Executing before start script")) + using (var op = logger.BeginOperation("Executing before start script")) { if (File.Exists(path)) { @@ -322,8 +311,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); script.AddVariable("RedistributableManifest", redistributableManifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("PlayerAlias", playerAlias); try @@ -341,7 +330,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) @@ -368,7 +357,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -381,7 +370,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No before start script found"); + logger?.LogTrace("No before start script found"); } op.Complete(); @@ -389,7 +378,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Before Start script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Before Start script"); } return result; @@ -406,7 +395,7 @@ namespace LANCommander.SDK.Services var path = ScriptHelper.GetScriptFilePath(installDirectory, redistributableId, Enums.ScriptType.AfterStop); - using (var op = _logger.BeginOperation("Executing after stop script")) + using (var op = logger.BeginOperation("Executing after stop script")) { if (File.Exists(path)) { @@ -418,8 +407,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); script.AddVariable("RedistributableManifest", redistributableManifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("PlayerAlias", GameService.GetPlayerAlias(installDirectory, gameId)); try @@ -437,7 +426,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) @@ -464,7 +453,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -477,7 +466,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No after stop script found"); + logger?.LogTrace("No after stop script found"); } op.Complete(); @@ -486,7 +475,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run an After Stop script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run an After Stop script"); } return result; @@ -503,7 +492,7 @@ namespace LANCommander.SDK.Services var path = ScriptHelper.GetScriptFilePath(installDirectory, redistributableId, Enums.ScriptType.NameChange); - using (var op = _logger.BeginOperation("Executing name change script")) + using (var op = logger.BeginOperation("Executing name change script")) { if (File.Exists(path)) { @@ -513,9 +502,9 @@ namespace LANCommander.SDK.Services oldName = string.Empty; if (!string.IsNullOrWhiteSpace(oldName)) - _logger?.LogTrace("Old Name: {OldName}", oldName); + logger?.LogTrace("Old Name: {OldName}", oldName); - _logger?.LogTrace("New Name: {NewName}", newName); + logger?.LogTrace("New Name: {NewName}", newName); var script = new PowerShellScript(Enums.ScriptType.NameChange); @@ -525,8 +514,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); script.AddVariable("RedistributableManifest", redistributableManifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("OldPlayerAlias", oldName); script.AddVariable("NewPlayerAlias", newName); @@ -545,7 +534,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any()) @@ -572,7 +561,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -585,7 +574,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No name change script found"); + logger?.LogTrace("No name change script found"); } op.Complete(); @@ -593,7 +582,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Name Change script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Name Change script"); } return result; @@ -611,7 +600,7 @@ namespace LANCommander.SDK.Services var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.Install); - using (var op = _logger.BeginOperation("Executing install script")) + using (var op = logger.BeginOperation("Executing install script")) { if (File.Exists(path)) { @@ -622,8 +611,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); if (manifest.CustomFields != null && manifest.CustomFields.Any()) { @@ -645,7 +634,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } try @@ -659,7 +648,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -672,7 +661,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No install script found for game"); + logger?.LogTrace("No install script found for game"); } op.Complete(); @@ -680,7 +669,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run an Install script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run an Install script"); } return result; @@ -695,7 +684,7 @@ namespace LANCommander.SDK.Services var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.Uninstall); - using (var op = _logger.BeginOperation("Executing uninstall script")) + using (var op = logger.BeginOperation("Executing uninstall script")) { if (File.Exists(path)) { @@ -706,8 +695,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); if (manifest.CustomFields != null && manifest.CustomFields.Any()) { @@ -729,7 +718,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } try @@ -743,7 +732,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -756,7 +745,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No uninstall script found"); + logger?.LogTrace("No uninstall script found"); } op.Complete(); @@ -764,7 +753,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to get an Uninstall script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to get an Uninstall script"); } return result; @@ -779,7 +768,7 @@ namespace LANCommander.SDK.Services var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.BeforeStart); - using (var op = _logger.BeginOperation("Executing before start script")) + using (var op = logger.BeginOperation("Executing before start script")) { if (File.Exists(path)) { @@ -791,8 +780,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("PlayerAlias", playerAlias); if (manifest.CustomFields != null && manifest.CustomFields.Any()) @@ -816,7 +805,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } try @@ -830,7 +819,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -843,7 +832,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No before start script found"); + logger?.LogTrace("No before start script found"); } op.Complete(); @@ -851,7 +840,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Before Start script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Before Start script"); } return result; @@ -866,7 +855,7 @@ namespace LANCommander.SDK.Services var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.AfterStop); - using (var op = _logger.BeginOperation("Executing after stop script")) + using (var op = logger.BeginOperation("Executing after stop script")) { if (File.Exists(path)) { @@ -877,8 +866,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("PlayerAlias", GameService.GetPlayerAlias(installDirectory, gameId)); if (manifest.CustomFields != null && manifest.CustomFields.Any()) @@ -901,7 +890,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } try @@ -915,7 +904,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -928,7 +917,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No after stop script found"); + logger?.LogTrace("No after stop script found"); } op.Complete(); @@ -937,7 +926,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run an After Stop script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run an After Stop script"); } return result; @@ -952,7 +941,7 @@ namespace LANCommander.SDK.Services var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.NameChange); var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - using (var op = _logger.BeginOperation("Executing name change script")) + using (var op = logger.BeginOperation("Executing name change script")) { if (File.Exists(path)) { @@ -962,9 +951,9 @@ namespace LANCommander.SDK.Services oldName = string.Empty; if (!string.IsNullOrWhiteSpace(oldName)) - _logger?.LogTrace("Old Name: {OldName}", oldName); + logger?.LogTrace("Old Name: {OldName}", oldName); - _logger?.LogTrace("New Name: {NewName}", newName); + logger?.LogTrace("New Name: {NewName}", newName); var script = new PowerShellScript(Enums.ScriptType.NameChange); @@ -973,8 +962,8 @@ namespace LANCommander.SDK.Services script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("OldPlayerAlias", oldName); script.AddVariable("NewPlayerAlias", newName); @@ -998,7 +987,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } script.UseFile(path); @@ -1016,7 +1005,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -1029,7 +1018,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No name change script found"); + logger?.LogTrace("No name change script found"); } op.Complete(); @@ -1037,7 +1026,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Name Change script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Name Change script"); } return result; @@ -1052,7 +1041,7 @@ namespace LANCommander.SDK.Services var path = ScriptHelper.GetScriptFilePath(installDirectory, gameId, Enums.ScriptType.KeyChange); var manifest = await ManifestHelper.ReadAsync(installDirectory, gameId); - using (var op = _logger.BeginOperation("Executing key change script")) + using (var op = logger.BeginOperation("Executing key change script")) { if (File.Exists(path)) { @@ -1061,12 +1050,12 @@ namespace LANCommander.SDK.Services if (Debug) script.DebugHandler.OnDebugStart = OnDebugStart; - _logger?.LogTrace("New key is {Key}", key); + logger?.LogTrace("New key is {Key}", key); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); - script.AddVariable("DefaultInstallDirectory", _client.DefaultInstallDirectory); - script.AddVariable("ServerAddress", _client.BaseUrl.ToString()); + script.AddVariable("DefaultInstallDirectory", config.InstallDirectories.FirstOrDefault()); + script.AddVariable("ServerAddress", connectionService.GetServerAddress()); script.AddVariable("AllocatedKey", key); if (manifest.CustomFields != null && manifest.CustomFields.Any()) @@ -1090,7 +1079,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } GameService.UpdateCurrentKey(installDirectory, gameId, key); @@ -1106,7 +1095,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not debug script"); + logger?.LogError(ex, "Could not debug script"); } bool handled = false; @@ -1119,7 +1108,7 @@ namespace LANCommander.SDK.Services } else { - _logger?.LogTrace("No key change script found"); + logger?.LogTrace("No key change script found"); } op.Complete(); @@ -1127,7 +1116,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Key Change script"); + logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Key Change script"); } return result; @@ -1137,7 +1126,7 @@ namespace LANCommander.SDK.Services { try { - using (var op = _logger.BeginOperation("Executing game package script")) + using (var op = logger.BeginOperation("Executing game package script")) { var script = new PowerShellScript(Enums.ScriptType.Package); @@ -1155,7 +1144,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not enrich logs"); + logger?.LogError(ex, "Could not enrich logs"); } return await script.ExecuteAsync(); @@ -1163,7 +1152,7 @@ namespace LANCommander.SDK.Services } catch (Exception ex) { - _logger?.LogError(ex, "Could not execute game package script"); + logger?.LogError(ex, "Could not execute game package script"); } return null; diff --git a/LANCommander.SDK/Services/ServerService.cs b/LANCommander.SDK/Services/ServerService.cs index 7a048d07..b4f40ab8 100644 --- a/LANCommander.SDK/Services/ServerService.cs +++ b/LANCommander.SDK/Services/ServerService.cs @@ -3,61 +3,71 @@ using Microsoft.Extensions.Logging; using System; using System.IO; using System.Threading.Tasks; +using LANCommander.SDK.Abstractions; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class ServerService + public class ServerService( + ILANCommanderConfiguration config, + ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - private Client Client { get; set; } - public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; public delegate void OnArchiveExtractionProgressHandler(long position, long length); public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; - public ServerService(Client client) - { - Client = client; - } - - public ServerService(Client client, ILogger logger) - { - Client = client; - _logger = logger; - } - public async Task ImportAsync(string archivePath) { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await Client.ChunkedUploadRequestAsync("", fs); + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); if (objectKey != Guid.Empty) - await Client.PostRequestAsync($"/api/Servers/Import/{objectKey}"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Servers/Import/{objectKey}") + .PostAsync(); } } + [Obsolete] public async Task ExportAsync(string destinationPath, Guid serverId) { - await Client.DownloadRequestAsync($"/Servers/{serverId}/Export/Full", destinationPath); + throw new NotImplementedException(); } public async Task UploadArchiveAsync(string archivePath, Guid serverId, string version, string changelog = "") { using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) { - var objectKey = await Client.ChunkedUploadRequestAsync("", fs); - + var objectKey = await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UploadInChunksAsync(config.UploadChunkSize, fs); + if (objectKey != Guid.Empty) - await Client.PostRequestAsync($"/api/Servers/UploadArchive", new UploadArchiveRequest - { - Id = serverId, - ObjectKey = objectKey, - Version = version, - Changelog = changelog, - }); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Servers/UploadArchive") + .AddBody(new UploadArchiveRequest + { + Id = serverId, + ObjectKey = objectKey, + Version = version, + Changelog = changelog, + }) + .PostAsync(); } } } diff --git a/LANCommander.SDK/Services/TagService.cs b/LANCommander.SDK/Services/TagService.cs index d02a2377..5b176525 100644 --- a/LANCommander.SDK/Services/TagService.cs +++ b/LANCommander.SDK/Services/TagService.cs @@ -1,39 +1,41 @@ using LANCommander.SDK.Models; -using Microsoft.Extensions.Logging; using System.Threading.Tasks; +using LANCommander.SDK.Factories; namespace LANCommander.SDK.Services { - public class TagService + public class TagService(ApiRequestFactory apiRequestFactory) { - private readonly ILogger _logger; - - private readonly Client _client; - - public TagService(Client client) - { - _client = client; - } - - public TagService(Client client, ILogger logger) - { - _client = client; - _logger = logger; - } - public async Task CreateAsync(Tag tag) { - return await _client.PostRequestAsync("/api/Tags", tag); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute("/api/Tags") + .AddBody(tag) + .PostAsync(); } public async Task UpdateAsync(Tag tag) { - return await _client.PostRequestAsync($"/api/Tags/{tag.Id}", tag); + return await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Tags/{tag.Id}") + .AddBody(tag) + .PostAsync(); } public async Task DeleteAsync(Tag tag) { - await _client.DeleteRequestAsync($"/api/Tags/{tag.Id}"); + await apiRequestFactory + .Create() + .UseAuthenticationToken() + .UseVersioning() + .UseRoute($"/api/Tags/{tag.Id}") + .DeleteAsync(); } } } diff --git a/LANCommander.SDK/TrackableStream.cs b/LANCommander.SDK/TrackableStream.cs index 38f88352..33de6536 100644 --- a/LANCommander.SDK/TrackableStream.cs +++ b/LANCommander.SDK/TrackableStream.cs @@ -7,6 +7,9 @@ namespace LANCommander.SDK { public delegate void OnProgressDelegate(long Position, long Length); public event OnProgressDelegate OnProgress = delegate { }; + + public delegate void OnCompleteDelegate(); + public event OnCompleteDelegate OnComplete = delegate { }; private long internalStreamProgress = 0; private Stream internalStream; private bool disposeStream = false; @@ -210,11 +213,17 @@ namespace LANCommander.SDK { internalStream.Write(array, offset, count); OnProgress(internalStream.Position, internalStream.Length); + + if (internalStream.Position == internalStream.Length) + OnComplete(); } else { base.Write(array, offset, count); - OnProgress(this.Position, this.Length); + OnProgress(Position, Length); + + if (Position == Length) + OnComplete(); } } @@ -240,11 +249,17 @@ namespace LANCommander.SDK { internalStream.WriteByte(value); OnProgress(internalStream.Position, internalStream.Length); + + if (internalStream.Position == internalStream.Length) + OnComplete(); } else { base.WriteByte(value); - OnProgress(this.Position, this.Length); + OnProgress(Position, Length); + + if (Position == Length) + OnComplete(); } } @@ -289,12 +304,18 @@ namespace LANCommander.SDK { r = internalStream.Read(array, offset, count); internalStreamProgress += r; - OnProgress(internalStreamProgress, this.Length); + OnProgress(internalStreamProgress, Length); + + if (internalStreamProgress == Length) + OnComplete(); } else { r = base.Read(array, offset, count); - OnProgress(this.Position, this.Length); + OnProgress(Position, Length); + + if (Position == Length) + OnComplete(); } return r; @@ -317,12 +338,18 @@ namespace LANCommander.SDK { r = internalStream.ReadByte(); internalStreamProgress += r; - OnProgress(internalStreamProgress, this.Length); + OnProgress(internalStreamProgress, Length); + + if (internalStreamProgress == Length) + OnComplete(); } else { r = base.ReadByte(); - OnProgress(this.Position, this.Length); + OnProgress(Position, Length); + + if (Position == Length) + OnComplete(); } return r; }