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