using System; using System.Collections.Specialized; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; // using Codice.Utils; // MIRROR CHANGE using Edgegap.Codice.Utils; // MIRROR CHANGE using UnityEngine; namespace Edgegap.Editor.Api { /// /// Handles base URL and common methods for all Edgegap APIs. /// public abstract class EdgegapApiBase { #region Vars private readonly HttpClient _httpClient = new HttpClient(); // Base address set // MIRROR CHANGE: Unity 2020 support protected ApiEnvironment SelectedApiEnvironment { get; } protected EdgegapWindowMetadata.LogLevel LogLevel { get; set; } protected bool IsLogLevelDebug => LogLevel == EdgegapWindowMetadata.LogLevel.Debug; /// Based on SelectedApiEnvironment. /// private string GetBaseUrl() => SelectedApiEnvironment == ApiEnvironment.Staging ? ApiEnvironment.Staging.GetApiUrl() : ApiEnvironment.Console.GetApiUrl(); #endregion // Vars /// "console" || "staging-console"? /// Without the "token " prefix, although we'll clear this if present /// You may want more-verbose logs other than errs protected EdgegapApiBase( ApiEnvironment apiEnvironment, string apiToken, EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) { this.SelectedApiEnvironment = apiEnvironment; this._httpClient.BaseAddress = new Uri($"{GetBaseUrl()}/"); this._httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); string cleanedApiToken = apiToken.Replace("token ", ""); // We already prefixed token below this._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", cleanedApiToken); this.LogLevel = logLevel; } #region HTTP Requests /// /// POST | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. /// /// /// Serialize to your model via Newtonsoft /// /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// protected async Task PostAsync(string relativePath = "", string json = "{}") { StringContent stringContent = CreateStringContent(json); Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize POST uri: Can't end with `/`. if (IsLogLevelDebug) Debug.Log($"PostAsync to: `{uri}` with json: `{json}`"); try { return await ExecuteRequestAsync(() => _httpClient.PostAsync(uri, stringContent)); } catch (Exception e) { Debug.LogError($"Error: {e}"); throw; } } /// /// PATCH | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. /// /// /// Serialize to your model via Newtonsoft /// /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// protected async Task PatchAsync(string relativePath = "", string json = "{}") { StringContent stringContent = CreateStringContent(json); Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize PATCH uri: Can't end with `/`. if (IsLogLevelDebug) Debug.Log($"PatchAsync to: `{uri}` with json: `{json}`"); // (!) As of 11/15/2023, .PatchAsync() is "unsupported by Unity" -- so we manually set the verb and SendAsync() // Create the request manually HttpRequestMessage patchRequest = new HttpRequestMessage(new HttpMethod("PATCH"), uri) { Content = stringContent, }; try { return await ExecuteRequestAsync(() => _httpClient.SendAsync(patchRequest)); } catch (Exception e) { Debug.LogError($"Error: {e}"); throw; } } /// /// GET | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. /// /// /// /// To append to the URL; eg: "foo=0&bar=1" /// (!) First query key should prefix nothing, as shown /// /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// protected async Task GetAsync(string relativePath = "", string customQuery = "") { string completeRelativeUri = prepareEdgegapUriWithQuery( relativePath, customQuery); if (IsLogLevelDebug) Debug.Log($"GetAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); try { return await ExecuteRequestAsync(() => _httpClient.GetAsync(completeRelativeUri)); } catch (Exception e) { Debug.LogError($"Error: {e}"); throw; } } /// /// DELETE | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. /// /// /// /// To append to the URL; eg: "foo=0&bar=1" /// (!) First query key should prefix nothing, as shown /// /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// protected async Task DeleteAsync(string relativePath = "", string customQuery = "") { string completeRelativeUri = prepareEdgegapUriWithQuery( relativePath, customQuery); if (IsLogLevelDebug) Debug.Log($"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); try { return await ExecuteRequestAsync(() => _httpClient.DeleteAsync(completeRelativeUri)); } catch (Exception e) { Debug.LogError($"Error: {e}"); throw; } } /// POST || GET /// /// /// private static async Task ExecuteRequestAsync( Func> requestFunc, CancellationToken cancellationToken = default) { HttpResponseMessage response = null; try { response = await requestFunc(); } catch (HttpRequestException e) { Debug.LogError($"HttpRequestException: {e.Message}"); return null; } catch (TaskCanceledException e) { if (cancellationToken.IsCancellationRequested) Debug.LogError("Task was cancelled by caller."); else Debug.LogError($"TaskCanceledException: Timeout - {e.Message}"); return null; } catch (Exception e) // Generic exception handler { Debug.LogError($"Unexpected error occurred: {e.Message}"); return null; } // Check for a successful status code if (response == null) { Debug.Log("!Success (null response) - returning 500"); return CreateUnknown500Err(); } if (!response.IsSuccessStatusCode) { HttpMethod httpMethod = response.RequestMessage.Method; Debug.Log($"!Success: {(short)response.StatusCode} {response.ReasonPhrase} - " + $"{httpMethod} | {response.RequestMessage.RequestUri}` - " + $"{response.Content?.ReadAsStringAsync().Result}"); } return response; } #endregion // HTTP Requests #region Utils /// Creates a UTF-8 encoded application/json + json obj /// Arbitrary json obj /// private StringContent CreateStringContent(string json = "{}") => new StringContent(json, Encoding.UTF8, "application/json"); // MIRROR CHANGE: 'new()' not supported in Unity 2020 private static HttpResponseMessage CreateUnknown500Err() => new HttpResponseMessage(HttpStatusCode.InternalServerError); // 500 - Unknown // MIRROR CHANGE: 'new()' not supported in Unity 2020 /// /// Merges Edgegap-required query params (source) -> merges with custom query -> normalizes. /// /// /// /// private string prepareEdgegapUriWithQuery(string relativePath, string customQuery) { // Create UriBuilder using the BaseAddress UriBuilder uriBuilder = new UriBuilder(_httpClient.BaseAddress); // Add the relative path to the UriBuilder's path uriBuilder.Path += relativePath; // Parse the existing query from the UriBuilder NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); // Add default "source=unity" param query["source"] = "unity"; // Parse and merge the custom query parameters NameValueCollection customParams = HttpUtility.ParseQueryString(customQuery); foreach (string key in customParams) { query[key] = customParams[key]; } // Set the merged query back to the UriBuilder uriBuilder.Query = query.ToString(); // Extract the complete relative URI and return it return uriBuilder.Uri.PathAndQuery; } #endregion // Utils } }