From 909038ff79a500ae2fc3542bdfc6d64ca0037016 Mon Sep 17 00:00:00 2001 From: Gordon Pedersen Date: Tue, 16 Jul 2024 12:15:56 +1000 Subject: [PATCH] Tidy up of the API service --- Classes/ApiService.cs | 309 +++++++++++---------------- Models/API/AddressResponseList.cs | 1 + Models/API/IOmgLolResponseData.cs | 1 + Models/API/OmgLolApiException.cs | 11 + Models/API/StatusPostResponseData.cs | 2 +- 5 files changed, 135 insertions(+), 189 deletions(-) create mode 100644 Models/API/OmgLolApiException.cs diff --git a/Classes/ApiService.cs b/Classes/ApiService.cs index 2863a2b..39da78e 100644 --- a/Classes/ApiService.cs +++ b/Classes/ApiService.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Neighbourhood.omg.lol.Models; +using System; using System.Diagnostics; using System.Net.Http.Json; using System.Text; using System.Text.Json; +using System.Threading; namespace Neighbourhood.omg.lol { @@ -19,13 +21,23 @@ namespace Neighbourhood.omg.lol _client.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue(App.Name, App.Version)); _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, +#if DEBUG WriteIndented = true +#else + WriteIndented = false +#endif }; AddToken(token); } + /// + /// Deserialize json convenience function with default serializer options + /// + /// The type to deserialize + /// The string to deserialize + /// The deserialized object if successful, otherwise default public T? Deserialize(string str) { - T? responseObj = default(T); + T? responseObj = default; try { responseObj = JsonSerializer.Deserialize(str, _serializerOptions); } @@ -36,166 +48,31 @@ namespace Neighbourhood.omg.lol return responseObj; } - public void AddToken(string? token = null) { - if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult(); - if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - } + #region Base Requests - public void RemoveToken() { - _client.DefaultRequestHeaders.Remove("Authorization"); - } - - private async Task Get(string uri, CancellationToken cancellationToken = default) where T : IOmgLolResponseData { - T? responseData = default(T); - try { - HttpResponseMessage response = await _client.GetAsync(uri, cancellationToken: cancellationToken); - if (response.IsSuccessStatusCode) { - string str = await response.Content.ReadAsStringAsync(); - try { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) { - responseData = responseObj!.Response; - } - } - catch (JsonException ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - Debug.WriteLine(str); - } - } - } - catch (Exception ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - } - - return responseData; - } - - private async Task Post(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData { - TResponse? responseData = default(TResponse); - try { - HttpResponseMessage response = await _client.PostAsJsonAsync(uri, data, _serializerOptions, cancellationToken: cancellationToken); - string str = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; - } - } - } - catch (Exception ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - } - - return responseData; - } - - private async Task PostBinary(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData { - TResponse? responseData = default(TResponse); - try { - if (fileResult != null) using (var fileStream = await fileResult.OpenReadAsync()) { - Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri); - if (string.IsNullOrEmpty(url.Query)) uri += "?binary"; - else if (!url.Query.Contains("binary")) uri += "&binary"; - - HttpContent fileStreamContent = new StreamContent(fileStream); - fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(fileResult.ContentType ?? "application/octet-stream"); - fileStreamContent.Headers.ContentLength = fileStream.Length; - HttpResponseMessage response = await _client.PostAsync(uri, fileStreamContent, cancellationToken: cancellationToken); - string str = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; - } - } - } - } - catch (Exception ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - } - - return responseData; - } - - private async Task PostMultipart(string uri, TData? data = null, FileResult? fileResult = null, CancellationToken cancellationToken = default) - where TResponse : IOmgLolResponseData where TData : class - { - if(fileResult != null) { - using (var fileStream = await fileResult.OpenReadAsync()) - return await PostMultipart(uri, data: data, fileStream: fileStream, fileName: fileResult.FileName, contentType: fileResult.ContentType); - } - else return await PostMultipart(uri, data, fileStream: null); - } - - private async Task PostMultipart(string uri, TData? data = null, Stream? fileStream = null, string? fileName = null, string? contentType = null, CancellationToken cancellationToken = default) - where TResponse : IOmgLolResponseData where TData : class + /// + /// Decode the response from an API call + /// + /// The type of response object we are trying to get + /// The raw Http Response Message + /// A cancellation token to cancel the operation + /// The decoded object if successfull, otherwise default + private async Task DecodeResponse(HttpResponseMessage response, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData { TResponse? responseData = default; try { - using (MultipartFormDataContent formData = new MultipartFormDataContent()) { - if(fileStream != null) { - HttpContent fileStreamContent = new StreamContent(fileStream); - fileStreamContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") { - Name = "\"file\"", - FileName = $"\"{fileName}\"" ?? "\"unknown\"" - }; - fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType ?? "application/octet-stream"); - formData.Add(fileStreamContent); - } - if (data != null) { - HttpContent jsonContent = JsonContent.Create(data, options: _serializerOptions); - formData.Add(jsonContent); - } - - HttpResponseMessage response = await _client.PostAsync(uri, formData, cancellationToken: cancellationToken); - string str = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; - } - } - } - } - catch (Exception ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - } - - return responseData; - } - - private async Task Put(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData { - TResponse? responseData = default(TResponse); - try { - string json = JsonSerializer.Serialize(data, _serializerOptions); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, uri); - request.Content = new StringContent(json, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken); - if (response.IsSuccessStatusCode) { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; - } - } - } - catch (Exception ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - } - - return responseData; - } - - private async Task Patch(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData { - TResponse? responseData = default(TResponse); - try { - HttpResponseMessage response = await _client.PatchAsJsonAsync(uri, data, _serializerOptions, cancellationToken: cancellationToken); string str = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; + OmgLolResponse? responseObj = Deserialize>(str); + if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) { + responseData = responseObj!.Response; } } + else { + OmgLolResponse? responseObj = Deserialize>(str); + throw responseObj == null ? new OmgLolApiException(str) : new OmgLolApiException(responseObj); + } } catch (Exception ex) { Debug.WriteLine(@"\tERROR {0}", ex.Message); @@ -204,23 +81,46 @@ namespace Neighbourhood.omg.lol return responseData; } - private async Task Delete(string uri, CancellationToken cancellationToken = default) where T : IOmgLolResponseData { - T? responseData = default(T); + /// + /// Performs a request for the supplied uri, with the supplied Http Method, + /// with the supplied data in the body (if present) + /// + /// The type of response we are expecting + /// The type of data we are sending + /// The uri to request + /// The Http Method to use for the request + /// The data to send in the body of the request + /// A FileResult for the file to send in the body of the request as binary data + /// A cancellation token + /// The returned data if successful, otherwise default + private async Task Request(string uri, HttpMethod method, TData? data = default, FileResult? file = null, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + { + TResponse? responseData = default; try { - HttpResponseMessage response = await _client.DeleteAsync(uri, cancellationToken: cancellationToken); - if (response.IsSuccessStatusCode) { - string str = await response.Content.ReadAsStringAsync(); - try { - OmgLolResponse? responseObj = await response.Content.ReadFromJsonAsync>(_serializerOptions, cancellationToken: cancellationToken); - if (responseObj?.Request?.Success ?? false) { - responseData = responseObj.Response; - } - } - catch (JsonException ex) { - Debug.WriteLine(@"\tERROR {0}", ex.Message); - Debug.WriteLine(str); - } + HttpRequestMessage request = new HttpRequestMessage(method, uri); + Stream? fileStream = null; + if (file != null) { + // append "binary" query parameter (if not already present) + Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri); + if (string.IsNullOrEmpty(url.Query)) uri += "?binary"; + else if (!url.Query.Contains("binary")) uri += "&binary"; + request = new HttpRequestMessage(method, uri); + + fileStream = await file.OpenReadAsync(); + HttpContent fileStreamContent = new StreamContent(fileStream); + fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType ?? "application/octet-stream"); + fileStreamContent.Headers.ContentLength = fileStream.Length; + request.Content = fileStreamContent; } + else if (data != null) { + string json = JsonSerializer.Serialize(data, _serializerOptions); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken); + responseData = await DecodeResponse(response, cancellationToken); + + fileStream?.Dispose(); } catch (Exception ex) { Debug.WriteLine(@"\tERROR {0}", ex.Message); @@ -229,6 +129,38 @@ namespace Neighbourhood.omg.lol return responseData; } + // GET request + private async Task Get(string uri, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Get, cancellationToken: cancellationToken); + + // POST request + private async Task Post(string uri, TData data, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Post, data: data, cancellationToken: cancellationToken); + + // POST request, but with a file as binary data + private async Task PostBinary(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Post, file: fileResult, cancellationToken: cancellationToken); + + // PUT request + private async Task Put(string uri, TData data, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Put, data: data, cancellationToken: cancellationToken); + + // PATCH request + private async Task Patch(string uri, TData data, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Patch, data: data, cancellationToken: cancellationToken); + + // Delete request + private async Task Delete(string uri, CancellationToken cancellationToken = default) + where TResponse : IOmgLolResponseData + => await Request(uri, HttpMethod.Delete, cancellationToken: cancellationToken); + #endregion + + #region Specific Requests public async Task> StatuslogLatest() => (await Get("/statuslog/latest"))?.Statuses ?? new List(); @@ -257,25 +189,6 @@ namespace Neighbourhood.omg.lol public async Task PutPic(string address, string base64Image) => (await Put($"/address/{address}/pics/upload", new PutPic { Pic = base64Image })); - public async Task PutPic(string address, IBrowserFile file) { - byte[] bytes; - using (var memoryStream = new MemoryStream()) { - await file.OpenReadStream().CopyToAsync(memoryStream); - bytes = memoryStream.ToArray(); - } - - return await PutPic(address, bytes); - } - - public async Task PutPic(string address, FileResult file) { - byte[] bytes; - using var memoryStream = new MemoryStream(); - using var fileStream = await file.OpenReadAsync(); - await fileStream.CopyToAsync(memoryStream); - bytes = memoryStream.ToArray(); - - return await PutPic(address, bytes); - } public async Task PutPic(string address, byte[] bytes) => await PutPic(address, Convert.ToBase64String(bytes)); @@ -324,7 +237,26 @@ namespace Neighbourhood.omg.lol (MarkupString)((await Get($"/theme/{theme}/preview"))?.Html ?? string.Empty); public async Task PostProfilePic(string address, FileResult image) => - await PostBinary($"/address/{address}/pfp", fileResult: image); + await PostBinary($"/address/{address}/pfp", fileResult: image); + + #endregion + + #region Auth + /// + /// Add the api token into the default headers + /// + /// The api token + public void AddToken(string? token = null) { + if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult(); + if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + /// + /// Remove the api token from the default headers + /// + public void RemoveToken() { + _client.DefaultRequestHeaders.Remove("Authorization"); + } public async Task OAuth(string code, string client_id, string client_secret, string redirect_uri) { string? token = null; @@ -343,6 +275,7 @@ namespace Neighbourhood.omg.lol } return token; } + #endregion public async Task GetHtml(string url) { string? raw = null; diff --git a/Models/API/AddressResponseList.cs b/Models/API/AddressResponseList.cs index ab787b3..33ab468 100644 --- a/Models/API/AddressResponseList.cs +++ b/Models/API/AddressResponseList.cs @@ -1,4 +1,5 @@ namespace Neighbourhood.omg.lol.Models { public class AddressResponseList : List, IOmgLolResponseList { + public string Message { get; set; } = string.Empty; } } diff --git a/Models/API/IOmgLolResponseData.cs b/Models/API/IOmgLolResponseData.cs index d126f13..8b4161c 100644 --- a/Models/API/IOmgLolResponseData.cs +++ b/Models/API/IOmgLolResponseData.cs @@ -1,4 +1,5 @@ namespace Neighbourhood.omg.lol.Models { public interface IOmgLolResponseData { + public string Message { get; set; } } } diff --git a/Models/API/OmgLolApiException.cs b/Models/API/OmgLolApiException.cs new file mode 100644 index 0000000..062b407 --- /dev/null +++ b/Models/API/OmgLolApiException.cs @@ -0,0 +1,11 @@ +namespace Neighbourhood.omg.lol.Models { + public class OmgLolApiException : Exception where T : IOmgLolResponseData { + public OmgLolResponse? Response { get; set; } + + public OmgLolApiException(OmgLolResponse? response) : base(response?.Response?.Message) { + Response = response; + } + + public OmgLolApiException(string? response) : base(response) { } + } +} diff --git a/Models/API/StatusPostResponseData.cs b/Models/API/StatusPostResponseData.cs index 05f34da..67ea2f5 100644 --- a/Models/API/StatusPostResponseData.cs +++ b/Models/API/StatusPostResponseData.cs @@ -1,6 +1,6 @@ namespace Neighbourhood.omg.lol.Models { public class StatusPostResponseData : IOmgLolResponseData { - public string? Message { get; set; } + public string Message { get; set; } = string.Empty; public string? Id { get; set; } public string? Url { get; set; } public string? ExternalUrl { get; set; }