From 7530c41b47743a55da656b46ec131af2df91b3c1 Mon Sep 17 00:00:00 2001 From: exsersewo Date: Tue, 19 Nov 2019 23:21:47 +0000 Subject: [PATCH] Add Imghoard API Wrapper to repo --- .../Miki.API.Images.Tests/ClientTests.cs | 39 ++++ .../Miki.API.Images.Tests.csproj | 23 ++ src/Miki.API.Images/Miki.API.Images.sln | 31 +++ src/Miki.API.Images/Miki.API.Images/Config.cs | 48 +++++ .../Exceptions/ResponseException.cs | 9 + .../Miki.API.Images/IImghoardClient.cs | 14 ++ .../Miki.API.Images/ImghoardClient.cs | 197 ++++++++++++++++++ .../Miki.API.Images/Miki.API.Images.csproj | 13 ++ .../Miki.API.Images/Models/Image.cs | 16 ++ .../Miki.API.Images/Models/ImagesResponse.cs | 37 ++++ .../Miki.API.Images/Models/PostImage.cs | 12 ++ .../Miki.API.Images/Models/SupportedImage.cs | 14 ++ .../Miki.API.Images/Models/UploadResponse.cs | 10 + 13 files changed, 463 insertions(+) create mode 100644 src/Miki.API.Images/Miki.API.Images.Tests/ClientTests.cs create mode 100644 src/Miki.API.Images/Miki.API.Images.Tests/Miki.API.Images.Tests.csproj create mode 100644 src/Miki.API.Images/Miki.API.Images.sln create mode 100644 src/Miki.API.Images/Miki.API.Images/Config.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Exceptions/ResponseException.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/IImghoardClient.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/ImghoardClient.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Miki.API.Images.csproj create mode 100644 src/Miki.API.Images/Miki.API.Images/Models/Image.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Models/ImagesResponse.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Models/PostImage.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Models/SupportedImage.cs create mode 100644 src/Miki.API.Images/Miki.API.Images/Models/UploadResponse.cs diff --git a/src/Miki.API.Images/Miki.API.Images.Tests/ClientTests.cs b/src/Miki.API.Images/Miki.API.Images.Tests/ClientTests.cs new file mode 100644 index 0000000..0b33f0f --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images.Tests/ClientTests.cs @@ -0,0 +1,39 @@ +using Miki.API.Images.Models; +using Moq; +using System.Threading.Tasks; +using Xunit; + +namespace Miki.API.Images.Tests +{ + public class ClientTests + { + [Fact] + public void ConstructClientTest() + { + var i = new ImghoardClient(); + + Assert.NotNull(i); + Assert.Equal(Config.Default().Endpoint, i.GetEndpoint()); + } + + [Fact] + public async Task GetSingleImageAsync() + { + var mock = new Mock(); + + mock.Setup(x => x.GetImageAsync(It.IsAny())) + .Returns(Task.FromResult(new Image + { + Id = 1171190856858734592, + Tags = new[] { "animal", "cat" }, + Url = "https://cdn.miki.ai/ext/imgh/1ciajYwALX.jpeg" + })); + + var response = await mock.Object.GetImageAsync(1171190856858734592); + + Assert.Equal(1171190856858734592, response.Id); + Assert.NotNull(response.Tags); + Assert.NotNull(response.Url); + } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images.Tests/Miki.API.Images.Tests.csproj b/src/Miki.API.Images/Miki.API.Images.Tests/Miki.API.Images.Tests.csproj new file mode 100644 index 0000000..5fe7951 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images.Tests/Miki.API.Images.Tests.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.0 + + Exe + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/Miki.API.Images/Miki.API.Images.sln b/src/Miki.API.Images/Miki.API.Images.sln new file mode 100644 index 0000000..fde84a2 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29424.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Miki.API.Images", "Miki.API.Images\Miki.API.Images.csproj", "{AA62EFC3-BA7E-4FAC-89C8-18ED66A5ED48}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Miki.API.Images.Tests", "Miki.API.Images.Tests\Miki.API.Images.Tests.csproj", "{D9B41632-B8D6-40A3-8C54-C95C1FF525E6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AA62EFC3-BA7E-4FAC-89C8-18ED66A5ED48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA62EFC3-BA7E-4FAC-89C8-18ED66A5ED48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA62EFC3-BA7E-4FAC-89C8-18ED66A5ED48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA62EFC3-BA7E-4FAC-89C8-18ED66A5ED48}.Release|Any CPU.Build.0 = Release|Any CPU + {D9B41632-B8D6-40A3-8C54-C95C1FF525E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9B41632-B8D6-40A3-8C54-C95C1FF525E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9B41632-B8D6-40A3-8C54-C95C1FF525E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9B41632-B8D6-40A3-8C54-C95C1FF525E6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1CE187D2-8805-4389-B106-7FD9891DCF1C} + EndGlobalSection +EndGlobal diff --git a/src/Miki.API.Images/Miki.API.Images/Config.cs b/src/Miki.API.Images/Miki.API.Images/Config.cs new file mode 100644 index 0000000..427e0b5 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Config.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; + +namespace Miki.API.Images +{ + public class Config : IEquatable + { + public string Tenancy { get; set; } = "production"; + public string Endpoint { get; set; } = "https://api.miki.ai/images"; + public string UserAgent { get; set; } = + $"Miki.API.Images/"+ + Assembly.GetExecutingAssembly() + .GetName() + .Version + .ToString() + .Substring(0, 3) + + " (https://github.com/Mikibot/dotnet-miki-api)"; + + /// + /// Allows the client to use unstable, experimental features + /// + public bool Experimental { get; set; } = false; + + public static Config Default() + { + return new Config(); + } + + public override bool Equals(object obj) + { + return Equals(obj as Config); + } + + public bool Equals(Config other) + { + return other != null && + Tenancy == other.Tenancy && + Endpoint == other.Endpoint && + UserAgent == other.UserAgent && + Experimental == other.Experimental; + } + + public override int GetHashCode() + { + return HashCode.Combine(Tenancy, Endpoint, UserAgent, Experimental); + } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Exceptions/ResponseException.cs b/src/Miki.API.Images/Miki.API.Images/Exceptions/ResponseException.cs new file mode 100644 index 0000000..92142ca --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Exceptions/ResponseException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Miki.API.Images.Exceptions +{ + public class ResponseException : Exception + { + public ResponseException(string reason = null, Exception innerException = null) : base(reason, innerException) { } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/IImghoardClient.cs b/src/Miki.API.Images/Miki.API.Images/IImghoardClient.cs new file mode 100644 index 0000000..38d010a --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/IImghoardClient.cs @@ -0,0 +1,14 @@ +using Miki.API.Images.Models; +using System; +using System.Threading.Tasks; + +namespace Miki.API.Images +{ + public interface IImghoardClient + { + Task GetImagesAsync(params string[] Tags); + Task GetImagesAsync(int page = 0, params string[] Tags); + Task GetImageAsync(ulong Id); + Task PostImageAsync(Memory bytes, params string[] Tags); + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/ImghoardClient.cs b/src/Miki.API.Images/Miki.API.Images/ImghoardClient.cs new file mode 100644 index 0000000..389ef58 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/ImghoardClient.cs @@ -0,0 +1,197 @@ +using Miki.API.Images.Exceptions; +using Miki.API.Images.Models; +using Miki.Utils.Imaging.Headers; +using Miki.Utils.Imaging.Headers.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Miki.API.Images +{ + public class ImghoardClient : IImghoardClient + { + private HttpClient apiClient; + private readonly Config config; + private const int Mb = 1000000; + private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings + { + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore + }; + + public ImghoardClient() : this(Config.Default()) { } + + public ImghoardClient(Config config) + { + this.config = config; + + this.apiClient = new HttpClient(); + this.apiClient.DefaultRequestHeaders.Add("x-miki-tenancy", config.Tenancy); + this.apiClient.DefaultRequestHeaders.Add("User-Agent", config.UserAgent); + } + + public string GetEndpoint() + => config.Endpoint; + + /// + /// Gets the first page of results given an array of Tags to find + /// + /// Tags to search for + /// A readonly list of images found with the Tags entered + public async Task GetImagesAsync(params string[] tags) + => await GetImagesAsync(0, tags); + + /// + /// Gets the given page of results given an array of Tags to find + /// + /// Tags to search for + /// A readonly list of images found with the Tags entered + public async Task GetImagesAsync(int page = 0, params string[] tags) + { + List args = new List(); + + if (page > 0) + { + args.Add($"page{page}"); + } + + if (tags.Any()) + { + args.Add(string.Join("+", tags)); + } + + string url = ""; + + if (args.Any()) + url = $"?{string.Join("&", args)}"; + + var response = await apiClient.GetAsync(config.Endpoint + url); + + if (response.IsSuccessStatusCode) + { + return new ImagesResponse(this, JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()), tags, page); + } + + throw new ResponseException("Response was not successfull; Reason: \"" + response.ReasonPhrase + "\""); + } + + /// + /// Get an image with a given Id + /// + /// The snowflake Id of the Image to get + /// The image with the given snowflake + public async Task GetImageAsync(ulong id) + { + var response = await apiClient.GetAsync(config.Endpoint + $"/{id}"); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + + throw new ResponseException(response.ReasonPhrase); + } + + /// + /// Posts a new image to the Imghoard instance + /// + /// The image stream to upload + /// The tags of the image being uploaded + /// The url of the uploaded image or null on failure + public async Task PostImageAsync(Stream image, params string[] tags) + { + byte[] bytes; + + using (var mStream = new MemoryStream()) + { + await image.CopyToAsync(mStream); + bytes = mStream.ToArray(); + } + image.Position = 0; + + return await PostImageAsync(bytes, tags); + } + + /// + /// Posts a new image to the Imghoard instance + /// + /// The raw bytes of the image to upload + /// The tags of the image being uploaded + /// The url of the uploaded image or null on failure + public async Task PostImageAsync(Memory bytes, params string[] tags) + { + if (bytes.Length >= Mb && !config.Experimental) + { + throw new NotSupportedException("In order to upload images larger than 1MB you need to enable experimental features in the config"); + } + + var check = IsSupported(bytes.Span); + + if (!check.Supported) + { + throw new NotSupportedException("You have given an incorrect image format, currently supported formats are: png, jpeg, gif"); + } + + if (bytes.Length < Mb) + { + var body = JsonConvert.SerializeObject( + new PostImage + { + Data = $"data:image/{check.Prefix};base64,{Convert.ToBase64String(bytes.Span)}", + Tags = tags + }, + serializerSettings + ); + + var response = await apiClient.PostAsync(config.Endpoint, new StringContent(body)); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()).File; + } + + throw new ResponseException("Response was not successfull; Reason: \"" + response.ReasonPhrase + "\""); + } + else + { + var body = new MultipartFormDataContent + { + { new StringContent($"image/{check.Prefix}"), "data-type" }, + { new ByteArrayContent(bytes.Span.ToArray()), "data" }, + { new StringContent(string.Join(",", tags)), "tags" } + }; + + var response = await apiClient.PostAsync(config.Endpoint, body); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()).File; + } + + throw new ResponseException("Response was not successfull; Reason: \"" + response.ReasonPhrase + "\""); + } + } + + private SupportedImage IsSupported(Span image) + { + if (ImageHeaders.Validate(image, ImageType.Png)) + { + return new SupportedImage(true, "png"); + } + if (ImageHeaders.Validate(image, ImageType.Jpeg)) + { + return new SupportedImage(true, "jpeg"); + } + if (ImageHeaders.Validate(image, ImageType.Gif89a) + || ImageHeaders.Validate(image, ImageType.Gif87a)) + { + return new SupportedImage(true, "gif"); + } + return new SupportedImage(false, null); + } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Miki.API.Images.csproj b/src/Miki.API.Images/Miki.API.Images/Miki.API.Images.csproj new file mode 100644 index 0000000..a5c10e6 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Miki.API.Images.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + + + + + + + + + diff --git a/src/Miki.API.Images/Miki.API.Images/Models/Image.cs b/src/Miki.API.Images/Miki.API.Images/Models/Image.cs new file mode 100644 index 0000000..ab9e6a8 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Models/Image.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Miki.API.Images.Tests")] +namespace Miki.API.Images.Models +{ + public class Image + { + [JsonProperty("ID")] + public ulong Id { get; internal set; } + [JsonProperty("Tags")] + public string[] Tags { get; internal set; } + [JsonProperty("URL")] + public string Url { get; internal set; } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Models/ImagesResponse.cs b/src/Miki.API.Images/Miki.API.Images/Models/ImagesResponse.cs new file mode 100644 index 0000000..b5e8364 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Models/ImagesResponse.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Miki.API.Images.Models +{ + public class ImagesResponse + { + private readonly ImghoardClient clientInstance; + + public IReadOnlyList Images { get; private set; } + public string[] QueryTags { get; private set; } + public int Page { get; private set; } + + /// + /// Creates a new instance of ImagesResponse + /// + /// An instance of the Imghoard Client + /// The list of images currently gotten + /// Query tags used to get the images + /// Current page the response is on + public ImagesResponse(ImghoardClient client, IEnumerable images, string[] queryTags, int page) + { + this.clientInstance = client; + this.Images = images.ToList(); + this.QueryTags = queryTags; + this.Page = page; + } + + /// + /// Get the next page using the same tags as before + /// + /// The next page of images + public async Task GetNextPageAsync() => + await clientInstance.GetImagesAsync(Page + 1, QueryTags); + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Models/PostImage.cs b/src/Miki.API.Images/Miki.API.Images/Models/PostImage.cs new file mode 100644 index 0000000..2bfdc7f --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Models/PostImage.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Miki.API.Images.Models +{ + internal class PostImage + { + [JsonProperty("Tags")] + public string[] Tags { get; internal set; } + [JsonProperty("Data")] + public string Data { get; internal set; } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Models/SupportedImage.cs b/src/Miki.API.Images/Miki.API.Images/Models/SupportedImage.cs new file mode 100644 index 0000000..1e09e3e --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Models/SupportedImage.cs @@ -0,0 +1,14 @@ +namespace Miki.API.Images.Models +{ + internal struct SupportedImage + { + public bool Supported { get; private set; } + public string Prefix { get; private set; } + + public SupportedImage(bool suported, string prefix) + { + this.Supported = suported; + this.Prefix = prefix; + } + } +} diff --git a/src/Miki.API.Images/Miki.API.Images/Models/UploadResponse.cs b/src/Miki.API.Images/Miki.API.Images/Models/UploadResponse.cs new file mode 100644 index 0000000..68fa340 --- /dev/null +++ b/src/Miki.API.Images/Miki.API.Images/Models/UploadResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Miki.API.Images.Models +{ + public class UploadResponse + { + [JsonProperty("File")] + public string File { get; internal set; } + } +}