Skip to content

Commit

Permalink
Add Imghoard API Wrapper to repo
Browse files Browse the repository at this point in the history
  • Loading branch information
exsersewo committed Nov 19, 2019
1 parent 7ac7c49 commit 7530c41
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 0 deletions.
39 changes: 39 additions & 0 deletions 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<IImghoardClient>();

mock.Setup(x => x.GetImageAsync(It.IsAny<ulong>()))
.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<ulong>(1171190856858734592, response.Id);
Assert.NotNull(response.Tags);
Assert.NotNull(response.Url);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<ApplicationIcon />
<OutputType>Exe</OutputType>
<StartupObject />
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Miki.API.Images\Miki.API.Images.csproj" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions 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
48 changes: 48 additions & 0 deletions 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<Config>
{
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)";

/// <summary>
/// Allows the client to use unstable, experimental features
/// </summary>
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);
}
}
}
@@ -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) { }
}
}
14 changes: 14 additions & 0 deletions 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<ImagesResponse> GetImagesAsync(params string[] Tags);
Task<ImagesResponse> GetImagesAsync(int page = 0, params string[] Tags);
Task<Image> GetImageAsync(ulong Id);
Task<string> PostImageAsync(Memory<byte> bytes, params string[] Tags);
}
}
197 changes: 197 additions & 0 deletions 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;

/// <summary>
/// Gets the first page of results given an array of Tags to find
/// </summary>
/// <param name="tags">Tags to search for</param>
/// <returns>A readonly list of images found with the Tags entered</returns>
public async Task<ImagesResponse> GetImagesAsync(params string[] tags)
=> await GetImagesAsync(0, tags);

/// <summary>
/// Gets the given page of results given an array of Tags to find
/// </summary>
/// <param name="tags">Tags to search for</param>
/// <returns>A readonly list of images found with the Tags entered</returns>
public async Task<ImagesResponse> GetImagesAsync(int page = 0, params string[] tags)
{
List<string> args = new List<string>();

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<IReadOnlyList<Image>>(await response.Content.ReadAsStringAsync()), tags, page);
}

throw new ResponseException("Response was not successfull; Reason: \"" + response.ReasonPhrase + "\"");
}

/// <summary>
/// Get an image with a given Id
/// </summary>
/// <param name="id">The snowflake Id of the Image to get</param>
/// <returns>The image with the given snowflake</returns>
public async Task<Image> GetImageAsync(ulong id)
{
var response = await apiClient.GetAsync(config.Endpoint + $"/{id}");

if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<Image>(await response.Content.ReadAsStringAsync());
}

throw new ResponseException(response.ReasonPhrase);
}

/// <summary>
/// Posts a new image to the Imghoard instance
/// </summary>
/// <param name="image">The image stream to upload</param>
/// <param name="tags">The tags of the image being uploaded</param>
/// <returns>The url of the uploaded image or null on failure</returns>
public async Task<string> 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);
}

/// <summary>
/// Posts a new image to the Imghoard instance
/// </summary>
/// <param name="bytes">The raw bytes of the image to upload</param>
/// <param name="tags">The tags of the image being uploaded</param>
/// <returns>The url of the uploaded image or null on failure</returns>
public async Task<string> PostImageAsync(Memory<byte> 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<UploadResponse>(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<UploadResponse>(await response.Content.ReadAsStringAsync()).File;
}

throw new ResponseException("Response was not successfull; Reason: \"" + response.ReasonPhrase + "\"");
}
}

private SupportedImage IsSupported(Span<byte> 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);
}
}
}
13 changes: 13 additions & 0 deletions src/Miki.API.Images/Miki.API.Images/Miki.API.Images.csproj
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Miki.Utils.Imaging.Headers" Version="1.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions 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; }
}
}

0 comments on commit 7530c41

Please sign in to comment.