diff --git a/src/Discord.Net.Rest/Net/HttpClientFactoryClient.cs b/src/Discord.Net.Rest/Net/HttpClientFactoryClient.cs new file mode 100644 index 000000000..b27918557 --- /dev/null +++ b/src/Discord.Net.Rest/Net/HttpClientFactoryClient.cs @@ -0,0 +1,179 @@ +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Rest.Net +{ + internal sealed class HttpClientFactoryClient : IRestClient, IDisposable + { + private const int HR_SECURECHANNELFAILED = -2146233079; + + private readonly HttpClient _client; + private readonly string _baseUrl; + private readonly JsonSerializer _errorDeserializer; + private CancellationToken _cancelToken; + private bool _isDisposed; + + public HttpClientFactoryClient(string baseUrl, HttpClient httpClient, bool useProxy = false) + { + _baseUrl = baseUrl; + //this client would be given to use through DI. The advantage would be that it would be managed by DI, and no socket Exhaustation possible. + _client = httpClient; +//#pragma warning disable IDISP014 +// _client = new HttpClient(new HttpClientHandler +// { +// AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, +// UseCookies = false, +// UseProxy = useProxy, +// }); +//#pragma warning restore IDISP014 + SetHeader("accept-encoding", "gzip, deflate"); + + _cancelToken = CancellationToken.None; + _errorDeserializer = new JsonSerializer(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + if (value != null) + _client.DefaultRequestHeaders.Add(key, value); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelToken = cancelToken; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) + restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) + restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + + /// Unsupported param type. + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) + restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + MemoryStream memoryStream = null; + if (multipartParams != null) + { + foreach (var p in multipartParams) + { + switch (p.Value) + { +#pragma warning disable IDISP004 + case string stringValue: + { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: + { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: + { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: + { + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; +#pragma warning disable IDISP001 + stream = memoryStream; +#pragma warning restore IDISP001 + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); +#pragma warning restore IDISP004 + continue; + } + default: + throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); + } + } + } + restRequest.Content = content; + var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + memoryStream?.Dispose(); + return result; + } + } + + private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + { + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken)) + { + cancelToken = cancelTokenSource.Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + + return new RestResponse(response.StatusCode, headers, stream); + } + } + + private static readonly HttpMethod Patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": + return HttpMethod.Delete; + case "GET": + return HttpMethod.Get; + case "PATCH": + return Patch; + case "POST": + return HttpMethod.Post; + case "PUT": + return HttpMethod.Put; + default: + throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + + } +} diff --git a/src/Discord.Net.Rest/Net/HttpClientFactoryClientProvider.cs b/src/Discord.Net.Rest/Net/HttpClientFactoryClientProvider.cs new file mode 100644 index 000000000..11e70dc0f --- /dev/null +++ b/src/Discord.Net.Rest/Net/HttpClientFactoryClientProvider.cs @@ -0,0 +1,27 @@ +using Discord.Net.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest.Net +{ + public class HttpFactoryRestClientProvider + { + public readonly RestClientProvider Instance; + + private readonly IHttpClientFactory _httpClientFactory; + + public HttpFactoryRestClientProvider(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + + Instance = url => new HttpClientFactoryClient(url, _httpClientFactory.CreateClient("HttpFactoryRestClientProvider"), useProxy: false); + } + + + + } +}