Added support for .NET Standard 1.1 and 1.2

This commit is contained in:
RogueException
2016-12-16 05:51:07 -04:00
parent a1addd4016
commit 8f87b2cc71
49 changed files with 533 additions and 348 deletions

View File

@@ -160,8 +160,8 @@ namespace Discord.API
LoginState = LoginState.LoggedOut;
}
internal virtual Task ConnectInternalAsync() => Task.CompletedTask;
internal virtual Task DisconnectInternalAsync() => Task.CompletedTask;
internal virtual Task ConnectInternalAsync() => Task.Delay(0);
internal virtual Task DisconnectInternalAsync() => Task.Delay(0);
//Core
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,

View File

@@ -1,60 +1,30 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Description>A .Net API wrapper and bot framework for Discord.</Description>
<VersionPrefix>1.0.0-beta2</VersionPrefix>
<TargetFramework>netstandard1.3</TargetFramework>
<TargetFrameworks>netstandard1.1;netstandard1.3</TargetFrameworks>
<AssemblyName>Discord.Net.Core</AssemblyName>
<PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl>
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8</PackageTargetFallback>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<EmbeddedResource Include="compiler\resources\**\*" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Primitives">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>9.0.1</Version>
</PackageReference>
<PackageReference Include="System.Collections.Concurrent">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="System.Collections.Immutable">
<Version>1.3.0</Version>
</PackageReference>
<PackageReference Include="System.Interactive.Async">
<Version>3.1.0</Version>
</PackageReference>
<PackageReference Include="System.Net.Http">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="System.Net.WebSockets.Client">
<Version>4.3.0</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Collections.Concurrent" Version="4.3.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.0" />
<PackageReference Include="System.Interactive.Async" Version="3.1.0" />
<PackageReference Include="System.Net.Http" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.1.1" />
</ItemGroup>
<ItemGroup />
<PropertyGroup Label="Configuration">
<SignAssembly>False</SignAssembly>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -9,8 +9,10 @@ namespace Discord
{
/// <summary> Sends a message to this message channel. </summary>
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null);
#if NETSTANDARD1_3
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null);
#endif
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null);

View File

@@ -8,24 +8,30 @@ namespace Discord
{
internal static class CollectionExtensions
{
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source)
=> new ConcurrentDictionaryWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
//public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IReadOnlyCollection<TValue> source)
// => new CollectionWrapper<TValue>(source, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this ICollection<TValue> source)
=> new CollectionWrapper<TValue>(source, () => source.Count);
//public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source)
// => new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IDictionary<TKey, TValue> source)
=> new CollectionWrapper<TValue>(source.Select(x => x.Value), () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source)
=> new ConcurrentDictionaryWrapper<TValue>(query, () => source.Count);
=> new CollectionWrapper<TValue>(query, () => source.Count);
public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue>(this IEnumerable<TValue> query, Func<int> countFunc)
=> new ConcurrentDictionaryWrapper<TValue>(query, countFunc);
=> new CollectionWrapper<TValue>(query, countFunc);
}
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
internal struct ConcurrentDictionaryWrapper<TValue> : IReadOnlyCollection<TValue>
internal struct CollectionWrapper<TValue> : IReadOnlyCollection<TValue>
{
private readonly IEnumerable<TValue> _query;
private readonly Func<int> _countFunc;
//It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected
public int Count => _countFunc();
public ConcurrentDictionaryWrapper(IEnumerable<TValue> query, Func<int> countFunc)
public CollectionWrapper(IEnumerable<TValue> query, Func<int> countFunc)
{
_query = query;
_countFunc = countFunc;

View File

@@ -17,56 +17,68 @@ namespace Discord.Logging
ClientLogger = new Logger(this, "Discord");
}
public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null)
{
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false);
}
public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null)
{
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false);
}
public async Task LogAsync(LogSeverity severity, string source, Exception ex)
{
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false);
}
public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null)
{
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false);
}
#if NETSTANDARD1_3
public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null)
{
if (severity <= Level)
await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false);
}
#endif
public Task ErrorAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Error, source, message, ex);
public Task ErrorAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Error, source, message, ex);
public Task ErrorAsync(string source, Exception ex)
=> LogAsync(LogSeverity.Error, source, ex);
public Task ErrorAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Error, source, message, ex);
#if NETSTANDARD1_3
public Task ErrorAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Error, source, message, ex);
#endif
public Task WarningAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Warning, source, message, ex);
public Task WarningAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Warning, source, message, ex);
public Task WarningAsync(string source, Exception ex)
=> LogAsync(LogSeverity.Warning, source, ex);
public Task WarningAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Warning, source, message, ex);
#if NETSTANDARD1_3
public Task WarningAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Warning, source, message, ex);
#endif
public Task InfoAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Info, source, message, ex);
public Task InfoAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Info, source, message, ex);
public Task InfoAsync(string source, Exception ex)
=> LogAsync(LogSeverity.Info, source, ex);
public Task InfoAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Info, source, message, ex);
#if NETSTANDARD1_3
public Task InfoAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Info, source, message, ex);
#endif
public Task VerboseAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Verbose, source, message, ex);
public Task VerboseAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Verbose, source, message, ex);
public Task VerboseAsync(string source, Exception ex)
=> LogAsync(LogSeverity.Verbose, source, ex);
public Task VerboseAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Verbose, source, message, ex);
#if NETSTANDARD1_3
public Task VerboseAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Verbose, source, message, ex);
#endif
public Task DebugAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Debug, source, message, ex);
public Task DebugAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Debug, source, message, ex);
public Task DebugAsync(string source, Exception ex)
=> LogAsync(LogSeverity.Debug, source, ex);
public Task DebugAsync(string source, string message, Exception ex = null)
=> LogAsync(LogSeverity.Debug, source, message, ex);
#if NETSTANDARD1_3
public Task DebugAsync(string source, FormattableString message, Exception ex = null)
=> LogAsync(LogSeverity.Debug, source, message, ex);
#endif
public Logger CreateLogger(string name) => new Logger(this, name);

View File

@@ -20,42 +20,54 @@ namespace Discord.Logging
=> _manager.LogAsync(severity, Name, exception);
public Task LogAsync(LogSeverity severity, string message, Exception exception = null)
=> _manager.LogAsync(severity, Name, message, exception);
#if NETSTANDARD1_3
public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null)
=> _manager.LogAsync(severity, Name, message, exception);
#endif
public Task ErrorAsync(string message, Exception exception = null)
=> _manager.ErrorAsync(Name, message, exception);
public Task ErrorAsync(FormattableString message, Exception exception = null)
=> _manager.ErrorAsync(Name, message, exception);
public Task ErrorAsync(Exception exception)
=> _manager.ErrorAsync(Name, exception);
public Task ErrorAsync(string message, Exception exception = null)
=> _manager.ErrorAsync(Name, message, exception);
#if NETSTANDARD1_3
public Task ErrorAsync(FormattableString message, Exception exception = null)
=> _manager.ErrorAsync(Name, message, exception);
#endif
public Task WarningAsync(string message, Exception exception = null)
=> _manager.WarningAsync(Name, message, exception);
public Task WarningAsync(FormattableString message, Exception exception = null)
=> _manager.WarningAsync(Name, message, exception);
public Task WarningAsync(Exception exception)
=> _manager.WarningAsync(Name, exception);
public Task WarningAsync(string message, Exception exception = null)
=> _manager.WarningAsync(Name, message, exception);
#if NETSTANDARD1_3
public Task WarningAsync(FormattableString message, Exception exception = null)
=> _manager.WarningAsync(Name, message, exception);
#endif
public Task InfoAsync(string message, Exception exception = null)
=> _manager.InfoAsync(Name, message, exception);
public Task InfoAsync(FormattableString message, Exception exception = null)
=> _manager.InfoAsync(Name, message, exception);
public Task InfoAsync(Exception exception)
=> _manager.InfoAsync(Name, exception);
public Task InfoAsync(string message, Exception exception = null)
=> _manager.InfoAsync(Name, message, exception);
#if NETSTANDARD1_3
public Task InfoAsync(FormattableString message, Exception exception = null)
=> _manager.InfoAsync(Name, message, exception);
#endif
public Task VerboseAsync(string message, Exception exception = null)
=> _manager.VerboseAsync(Name, message, exception);
public Task VerboseAsync(FormattableString message, Exception exception = null)
=> _manager.VerboseAsync(Name, message, exception);
public Task VerboseAsync(Exception exception)
=> _manager.VerboseAsync(Name, exception);
public Task VerboseAsync(string message, Exception exception = null)
=> _manager.VerboseAsync(Name, message, exception);
#if NETSTANDARD1_3
public Task VerboseAsync(FormattableString message, Exception exception = null)
=> _manager.VerboseAsync(Name, message, exception);
#endif
public Task DebugAsync(string message, Exception exception = null)
=> _manager.DebugAsync(Name, message, exception);
public Task DebugAsync(FormattableString message, Exception exception = null)
=> _manager.DebugAsync(Name, message, exception);
public Task DebugAsync(Exception exception)
=> _manager.DebugAsync(Name, exception);
public Task DebugAsync(string message, Exception exception = null)
=> _manager.DebugAsync(Name, message, exception);
#if NETSTANDARD1_3
public Task DebugAsync(FormattableString message, Exception exception = null)
=> _manager.DebugAsync(Name, message, exception);
#endif
}
}

View File

@@ -209,7 +209,7 @@ namespace Discord.Net.Queue
#endif
}
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var now = DateTimeUtils.ToUnixSeconds(DateTimeOffset.UtcNow);
DateTimeOffset? resetTick = null;
//Using X-RateLimit-Remaining causes a race condition

View File

@@ -17,7 +17,7 @@ namespace Discord.Net
IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) ? bool.Parse(temp) : false;
Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) ? int.Parse(temp) : (int?)null;
Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) ? int.Parse(temp) : (int?)null;
Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) ? DateTimeOffset.FromUnixTimeSeconds(int.Parse(temp)) : (DateTimeOffset?)null;
Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) ? DateTimeUtils.FromUnixSeconds(int.Parse(temp)) : (DateTimeOffset?)null;
RetryAfter = headers.TryGetValue("Retry-After", out temp) ? int.Parse(temp) : (int?)null;
}
}

View File

@@ -1,144 +0,0 @@
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.Net.Rest
{
public sealed class DefaultRestClient : IRestClient
{
private const int HR_SECURECHANNELFAILED = -2146233079;
private readonly HttpClient _client;
private readonly string _baseUrl;
private readonly JsonSerializer _errorDeserializer;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken;
private bool _isDisposed;
public DefaultRestClient(string baseUrl)
{
_baseUrl = baseUrl;
_client = new HttpClient(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false,
UseProxy = false
});
SetHeader("accept-encoding", "gzip, deflate");
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationToken.None;
_parentToken = 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)
{
_parentToken = cancelToken;
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token;
}
public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
if (multipartParams != null)
{
foreach (var p in multipartParams)
{
//TODO: C#7 Typeswitch candidate
var stringValue = p.Value as string;
if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; }
var byteArrayValue = p.Value as byte[];
if (byteArrayValue != null) { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; }
var streamValue = p.Value as Stream;
if (streamValue != null) { content.Add(new StreamContent(streamValue), p.Key); continue; }
if (p.Value is MultipartFile)
{
var fileValue = (MultipartFile)p.Value;
content.Add(new StreamContent(fileValue.Stream), p.Key, fileValue.Filename);
continue;
}
throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"");
}
}
restRequest.Content = content;
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
private async Task<RestResponse> SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly)
{
while (true)
{
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).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}");
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Net.Udp
{
public interface IUdpSocket
{
event Func<byte[], int, int, Task> ReceivedDatagram;
void SetCancelToken(CancellationToken cancelToken);
void SetDestination(string host, int port);
Task StartAsync();
Task StopAsync();
Task SendAsync(byte[] data, int index, int count);
}
}

View File

@@ -0,0 +1,4 @@
namespace Discord.Net.Udp
{
public delegate IUdpSocket UdpSocketProvider();
}

View File

@@ -1,219 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Net.WebSockets
{
public class DefaultWebSocketClient : IWebSocketClient
{
public const int ReceiveChunkSize = 16 * 1024; //16KB
public const int SendChunkSize = 4 * 1024; //4KB
private const int HR_TIMEOUT = -2147012894;
public event Func<byte[], int, int, Task> BinaryMessage;
public event Func<string, Task> TextMessage;
public event Func<Exception, Task> Closed;
private readonly SemaphoreSlim _sendLock;
private readonly Dictionary<string, string> _headers;
private ClientWebSocket _client;
private Task _task;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken;
private bool _isDisposed;
public DefaultWebSocketClient()
{
_sendLock = new SemaphoreSlim(1, 1);
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationToken.None;
_parentToken = CancellationToken.None;
_headers = new Dictionary<string, string>();
}
private void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
_client.Dispose();
_isDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
}
public async Task ConnectAsync(string host)
{
await _sendLock.WaitAsync().ConfigureAwait(false);
try
{
await ConnectInternalAsync(host).ConfigureAwait(false);
}
finally
{
_sendLock.Release();
}
}
private async Task ConnectInternalAsync(string host)
{
await DisconnectInternalAsync().ConfigureAwait(false);
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token;
_client = new ClientWebSocket();
_client.Options.Proxy = null;
_client.Options.KeepAliveInterval = TimeSpan.Zero;
foreach (var header in _headers)
{
if (header.Value != null)
_client.Options.SetRequestHeader(header.Key, header.Value);
}
await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false);
_task = RunAsync(_cancelToken);
}
public async Task DisconnectAsync()
{
await _sendLock.WaitAsync().ConfigureAwait(false);
try
{
await DisconnectInternalAsync().ConfigureAwait(false);
}
finally
{
_sendLock.Release();
}
}
private async Task DisconnectInternalAsync()
{
try { _cancelTokenSource.Cancel(false); } catch { }
await (_task ?? Task.CompletedTask).ConfigureAwait(false);
if (_client != null && _client.State == WebSocketState.Open)
{
var token = new CancellationToken();
await _client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", token);
_client.Dispose();
_client = null;
}
}
public void SetHeader(string key, string value)
{
_headers[key] = value;
}
public void SetCancelToken(CancellationToken cancelToken)
{
_parentToken = cancelToken;
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token;
}
public async Task SendAsync(byte[] data, int index, int count, bool isText)
{
await _sendLock.WaitAsync().ConfigureAwait(false);
try
{
if (_client == null) return;
int frameCount = (int)Math.Ceiling((double)count / SendChunkSize);
for (int i = 0; i < frameCount; i++, index += SendChunkSize)
{
bool isLast = i == (frameCount - 1);
int frameSize;
if (isLast)
frameSize = count - (i * SendChunkSize);
else
frameSize = SendChunkSize;
var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary;
await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false);
}
}
finally
{
_sendLock.Release();
}
}
private async Task RunAsync(CancellationToken cancelToken)
{
var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]);
try
{
while (!cancelToken.IsCancellationRequested)
{
WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false);
byte[] result;
int resultCount;
if (socketResult.MessageType == WebSocketMessageType.Close)
{
var _ = Closed(new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription));
return;
}
if (!socketResult.EndOfMessage)
{
//This is a large message (likely just READY), lets create a temporary expandable stream
using (var stream = new MemoryStream())
{
stream.Write(buffer.Array, 0, socketResult.Count);
do
{
if (cancelToken.IsCancellationRequested) return;
socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false);
stream.Write(buffer.Array, 0, socketResult.Count);
}
while (socketResult == null || !socketResult.EndOfMessage);
//Use the internal buffer if we can get it
resultCount = (int)stream.Length;
ArraySegment<byte> streamBuffer;
if (stream.TryGetBuffer(out streamBuffer))
result = streamBuffer.Array;
else
result = stream.ToArray();
}
}
else
{
//Small message
resultCount = socketResult.Count;
result = buffer.Array;
}
if (socketResult.MessageType == WebSocketMessageType.Text)
{
string text = Encoding.UTF8.GetString(result, 0, resultCount);
await TextMessage(text).ConfigureAwait(false);
}
else
await BinaryMessage(result, 0, resultCount).ConfigureAwait(false);
}
}
catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT)
{
var _ = Closed(new Exception("Connection timed out.", ex));
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
//This cannot be awaited otherwise we'll deadlock when DiscordApiClient waits for this task to complete.
var _ = Closed(ex);
}
}
}
}

View File

@@ -2,14 +2,58 @@
namespace Discord
{
internal static class DateTimeUtils
{
public static DateTimeOffset FromSnowflake(ulong value)
=> DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL));
internal static class DateTimeUtils
{
#if !NETSTANDARD1_3
//https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs
private const long UnixEpochTicks = 621355968000000000;
private const long UnixEpochSeconds = 62135596800;
#endif
public static DateTimeOffset FromTicks(long ticks)
=> new DateTimeOffset(ticks, TimeSpan.Zero);
public static DateTimeOffset? FromTicks(long? ticks)
=> ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null;
}
public static DateTimeOffset FromSnowflake(ulong value)
=> FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL));
public static DateTimeOffset FromTicks(long ticks)
=> new DateTimeOffset(ticks, TimeSpan.Zero);
public static DateTimeOffset? FromTicks(long? ticks)
=> ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null;
public static DateTimeOffset FromUnixSeconds(long seconds)
{
#if NETSTANDARD1_3
return DateTimeOffset.FromUnixTimeSeconds(seconds);
#else
long ticks = seconds * TimeSpan.TicksPerSecond + UnixEpochTicks;
return new DateTimeOffset(ticks, TimeSpan.Zero);
#endif
}
public static DateTimeOffset FromUnixMilliseconds(long seconds)
{
#if NETSTANDARD1_3
return DateTimeOffset.FromUnixTimeMilliseconds(seconds);
#else
long ticks = seconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks;
return new DateTimeOffset(ticks, TimeSpan.Zero);
#endif
}
public static long ToUnixSeconds(DateTimeOffset dto)
{
#if NETSTANDARD1_3
return dto.ToUnixTimeSeconds();
#else
long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerSecond;
return seconds - UnixEpochSeconds;
#endif
}
public static long ToUnixMilliseconds(DateTimeOffset dto)
{
#if NETSTANDARD1_3
return dto.ToUnixTimeMilliseconds();
#else
long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond;
return seconds - UnixEpochSeconds;
#endif
}
}
}

View File

@@ -26,25 +26,16 @@
},
"dependencies": {
"Microsoft.Win32.Primitives": "4.3.0",
"Newtonsoft.Json": "9.0.1",
"System.Collections.Concurrent": "4.3.0",
"System.Collections.Immutable": "1.3.0",
"System.Interactive.Async": "3.1.0",
"System.Net.Http": "4.3.0",
"System.Net.WebSockets.Client": {
"version": "4.3.0",
"type": "build"
}
"System.Runtime.Serialization.Primitives": "4.1.1"
},
"frameworks": {
"netstandard1.3": {
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
}
"netstandard1.1": {},
"netstandard1.3": {}
}
}