@@ -32,26 +32,29 @@ namespace Discord.API | |||||
protected readonly JsonSerializer _serializer; | protected readonly JsonSerializer _serializer; | ||||
protected readonly SemaphoreSlim _stateLock; | protected readonly SemaphoreSlim _stateLock; | ||||
private readonly RestClientProvider _restClientProvider; | private readonly RestClientProvider _restClientProvider; | ||||
private readonly string _userAgent; | |||||
protected string _authToken; | protected string _authToken; | ||||
protected bool _isDisposed; | protected bool _isDisposed; | ||||
private CancellationTokenSource _loginCancelToken; | private CancellationTokenSource _loginCancelToken; | ||||
private IRestClient _restClient; | private IRestClient _restClient; | ||||
private bool _fetchCurrentUser; | |||||
public RetryMode DefaultRetryMode { get; } | |||||
public string UserAgent { get; } | |||||
public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
public TokenType AuthTokenType { get; private set; } | public TokenType AuthTokenType { get; private set; } | ||||
public User CurrentUser { get; private set; } | public User CurrentUser { get; private set; } | ||||
public RequestQueue RequestQueue { get; private set; } | public RequestQueue RequestQueue { get; private set; } | ||||
internal bool FetchCurrentUser { get; set; } | |||||
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, | |||||
JsonSerializer serializer = null, RequestQueue requestQueue = null, bool fetchCurrentUser = true) | |||||
{ | { | ||||
_restClientProvider = restClientProvider; | _restClientProvider = restClientProvider; | ||||
_userAgent = userAgent; | |||||
UserAgent = userAgent; | |||||
_serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | ||||
RequestQueue = requestQueue; | RequestQueue = requestQueue; | ||||
FetchCurrentUser = true; | |||||
_fetchCurrentUser = fetchCurrentUser; | |||||
_stateLock = new SemaphoreSlim(1, 1); | _stateLock = new SemaphoreSlim(1, 1); | ||||
@@ -61,7 +64,7 @@ namespace Discord.API | |||||
{ | { | ||||
_restClient = _restClientProvider(baseUrl); | _restClient = _restClientProvider(baseUrl); | ||||
_restClient.SetHeader("accept", "*/*"); | _restClient.SetHeader("accept", "*/*"); | ||||
_restClient.SetHeader("user-agent", _userAgent); | |||||
_restClient.SetHeader("user-agent", UserAgent); | |||||
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | ||||
} | } | ||||
internal static string GetPrefixedToken(TokenType tokenType, string token) | internal static string GetPrefixedToken(TokenType tokenType, string token) | ||||
@@ -120,8 +123,8 @@ namespace Discord.API | |||||
_authToken = token; | _authToken = token; | ||||
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | ||||
if (FetchCurrentUser) | |||||
CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true }).ConfigureAwait(false); | |||||
if (_fetchCurrentUser) | |||||
CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); | |||||
LoginState = LoginState.LoggedIn; | LoginState = LoginState.LoggedIn; | ||||
} | } | ||||
@@ -257,6 +260,8 @@ namespace Discord.API | |||||
{ | { | ||||
if (!request.Options.IgnoreState) | if (!request.Options.IgnoreState) | ||||
CheckState(); | CheckState(); | ||||
if (request.Options.RetryMode == null) | |||||
request.Options.RetryMode = DefaultRetryMode; | |||||
var stopwatch = Stopwatch.StartNew(); | var stopwatch = Stopwatch.StartNew(); | ||||
var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); | var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); | ||||
@@ -19,6 +19,9 @@ namespace Discord | |||||
public const int MaxMessagesPerBatch = 100; | public const int MaxMessagesPerBatch = 100; | ||||
public const int MaxUsersPerBatch = 1000; | public const int MaxUsersPerBatch = 1000; | ||||
/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary> | |||||
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; | |||||
/// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> | /// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> | ||||
public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | ||||
} | } | ||||
@@ -12,6 +12,7 @@ namespace Discord | |||||
DateTimeOffset? JoinedAt { get; } | DateTimeOffset? JoinedAt { get; } | ||||
/// <summary> Gets the nickname for this user. </summary> | /// <summary> Gets the nickname for this user. </summary> | ||||
string Nickname { get; } | string Nickname { get; } | ||||
/// <summary> Gets the guild-level permissions for this user. </summary> | |||||
GuildPermissions GuildPermissions { get; } | GuildPermissions GuildPermissions { get; } | ||||
/// <summary> Gets the guild for this user. </summary> | /// <summary> Gets the guild for this user. </summary> | ||||
@@ -1,6 +1,8 @@ | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
#if DEBUG_LIMITS | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
#endif | |||||
using System.IO; | using System.IO; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Threading; | using System.Threading; | ||||
@@ -63,7 +65,11 @@ namespace Discord.Net.Queue | |||||
public async Task<Stream> SendAsync(RestRequest request) | public async Task<Stream> SendAsync(RestRequest request) | ||||
{ | { | ||||
request.CancelToken = _requestCancelToken; | |||||
if (request.Options.CancelToken.CanBeCanceled) | |||||
request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token; | |||||
else | |||||
request.Options.CancelToken = _requestCancelToken; | |||||
var bucket = GetOrCreateBucket(request.Options.BucketId, request); | var bucket = GetOrCreateBucket(request.Options.BucketId, request); | ||||
return await bucket.SendAsync(request).ConfigureAwait(false); | return await bucket.SendAsync(request).ConfigureAwait(false); | ||||
} | } | ||||
@@ -1,5 +1,4 @@ | |||||
using Discord.Net.Rest; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using System; | using System; | ||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
@@ -88,7 +87,10 @@ namespace Discord.Net.Queue | |||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
Debug.WriteLine($"[{id}] (!) 502"); | Debug.WriteLine($"[{id}] (!) 502"); | ||||
#endif | #endif | ||||
continue; //Continue | |||||
if ((request.Options.RetryMode & RetryMode.Retry502) == 0) | |||||
throw new HttpException(HttpStatusCode.BadGateway, null); | |||||
continue; //Retry | |||||
default: | default: | ||||
string reason = null; | string reason = null; | ||||
if (response.Stream != null) | if (response.Stream != null) | ||||
@@ -115,13 +117,28 @@ namespace Discord.Net.Queue | |||||
return response.Stream; | return response.Stream; | ||||
} | } | ||||
} | } | ||||
catch (TimeoutException) | |||||
{ | |||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
catch | |||||
Debug.WriteLine($"[{id}] Timeout"); | |||||
#endif | |||||
if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) | |||||
throw; | |||||
await Task.Delay(500); | |||||
continue; //Retry | |||||
} | |||||
catch (Exception) | |||||
{ | { | ||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Error"); | Debug.WriteLine($"[{id}] Error"); | ||||
throw; | |||||
} | |||||
#endif | #endif | ||||
if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) | |||||
throw; | |||||
await Task.Delay(500); | |||||
continue; //Retry | |||||
} | |||||
finally | finally | ||||
{ | { | ||||
UpdateRateLimit(id, request, info, lag, false); | UpdateRateLimit(id, request, info, lag, false); | ||||
@@ -140,7 +157,7 @@ namespace Discord.Net.Queue | |||||
while (true) | while (true) | ||||
{ | { | ||||
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested) | |||||
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) | |||||
{ | { | ||||
if (!isRateLimited) | if (!isRateLimited) | ||||
throw new TimeoutException(); | throw new TimeoutException(); | ||||
@@ -162,6 +179,10 @@ namespace Discord.Net.Queue | |||||
isRateLimited = true; | isRateLimited = true; | ||||
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); | await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); | ||||
} | } | ||||
if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | |||||
throw new RateLimitedException(); | |||||
if (resetAt.HasValue) | if (resetAt.HasValue) | ||||
{ | { | ||||
if (resetAt > timeoutAt) | if (resetAt > timeoutAt) | ||||
@@ -171,7 +192,7 @@ namespace Discord.Net.Queue | |||||
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); | Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); | ||||
#endif | #endif | ||||
if (millis > 0) | if (millis > 0) | ||||
await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); | |||||
await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false); | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -180,7 +201,7 @@ namespace Discord.Net.Queue | |||||
#if DEBUG_LIMITS | #if DEBUG_LIMITS | ||||
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); | Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); | ||||
#endif | #endif | ||||
await Task.Delay(500, request.CancelToken).ConfigureAwait(false); | |||||
await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); | |||||
} | } | ||||
continue; | continue; | ||||
} | } | ||||
@@ -15,7 +15,7 @@ namespace Discord.Net.Queue | |||||
public override async Task<RestResponse> SendAsync() | public override async Task<RestResponse> SendAsync() | ||||
{ | { | ||||
return await Client.SendAsync(Method, Endpoint, Json, CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -16,7 +16,7 @@ namespace Discord.Net.Queue | |||||
public override async Task<RestResponse> SendAsync() | public override async Task<RestResponse> SendAsync() | ||||
{ | { | ||||
return await Client.SendAsync(Method, Endpoint, MultipartParams, CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,7 +1,6 @@ | |||||
using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
using System; | using System; | ||||
using System.IO; | using System.IO; | ||||
using System.Threading; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Discord.Net.Queue | namespace Discord.Net.Queue | ||||
@@ -14,7 +13,6 @@ namespace Discord.Net.Queue | |||||
public DateTimeOffset? TimeoutAt { get; } | public DateTimeOffset? TimeoutAt { get; } | ||||
public TaskCompletionSource<Stream> Promise { get; } | public TaskCompletionSource<Stream> Promise { get; } | ||||
public RequestOptions Options { get; } | public RequestOptions Options { get; } | ||||
public CancellationToken CancelToken { get; internal set; } | |||||
public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) | public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) | ||||
{ | { | ||||
@@ -24,14 +22,13 @@ namespace Discord.Net.Queue | |||||
Method = method; | Method = method; | ||||
Endpoint = endpoint; | Endpoint = endpoint; | ||||
Options = options; | Options = options; | ||||
CancelToken = CancellationToken.None; | |||||
TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; | TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; | ||||
Promise = new TaskCompletionSource<Stream>(); | Promise = new TaskCompletionSource<Stream>(); | ||||
} | } | ||||
public virtual async Task<RestResponse> SendAsync() | public virtual async Task<RestResponse> SendAsync() | ||||
{ | { | ||||
return await Client.SendAsync(Method, Endpoint, CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,11 +1,18 @@ | |||||
namespace Discord | |||||
using System.Threading; | |||||
namespace Discord | |||||
{ | { | ||||
public class RequestOptions | public class RequestOptions | ||||
{ | { | ||||
public static RequestOptions Default => new RequestOptions(); | public static RequestOptions Default => new RequestOptions(); | ||||
/// <summary> The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. </summary> | |||||
/// <summary> | |||||
/// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. | |||||
/// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. | |||||
/// </summary> | |||||
public int? Timeout { get; set; } | public int? Timeout { get; set; } | ||||
public CancellationToken CancelToken { get; set; } = CancellationToken.None; | |||||
public RetryMode? RetryMode { get; set; } | |||||
public bool HeaderOnly { get; internal set; } | public bool HeaderOnly { get; internal set; } | ||||
internal bool IgnoreState { get; set; } | internal bool IgnoreState { get; set; } | ||||
@@ -13,7 +20,7 @@ | |||||
internal bool IsClientBucket { get; set; } | internal bool IsClientBucket { get; set; } | ||||
internal static RequestOptions CreateOrClone(RequestOptions options) | internal static RequestOptions CreateOrClone(RequestOptions options) | ||||
{ | |||||
{ | |||||
if (options == null) | if (options == null) | ||||
return new RequestOptions(); | return new RequestOptions(); | ||||
else | else | ||||
@@ -0,0 +1,22 @@ | |||||
using System; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> Specifies how a request should act in the case of an error. </summary> | |||||
[Flags] | |||||
public enum RetryMode | |||||
{ | |||||
/// <summary> If a request fails, an exception is thrown immediately. </summary> | |||||
AlwaysFail = 0x0, | |||||
/// <summary> Retry if a request timed out. </summary> | |||||
RetryTimeouts = 0x1, | |||||
/// <summary> Retry if a request failed due to a network error. </summary> | |||||
RetryErrors = 0x2, | |||||
/// <summary> Retry if a request failed due to a ratelimit. </summary> | |||||
RetryRatelimit = 0x4, | |||||
/// <summary> Retry if a request failed due to an HTTP error 502. </summary> | |||||
Retry502 = 0x8, | |||||
/// <summary> Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error. </summary> | |||||
AlwaysRetry = RetryTimeouts | RetryErrors | RetryRatelimit | Retry502, | |||||
} | |||||
} |
@@ -69,15 +69,13 @@ namespace Discord.API | |||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, | public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, | ||||
JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
: base(restClientProvider, userAgent, serializer, requestQueue) | |||||
RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
: base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, false) | |||||
{ | { | ||||
_connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
_clientId = clientId; | _clientId = clientId; | ||||
_origin = origin; | _origin = origin; | ||||
FetchCurrentUser = false; | |||||
_requestQueue = requestQueue ?? new RequestQueue(); | _requestQueue = requestQueue ?? new RequestQueue(); | ||||
_requests = new ConcurrentDictionary<Guid, RpcRequest>(); | _requests = new ConcurrentDictionary<Guid, RpcRequest>(); | ||||
@@ -32,11 +32,12 @@ namespace Discord.API | |||||
public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
: base(restClientProvider, userAgent, serializer, requestQueue) | |||||
public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, | |||||
RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null) | |||||
: base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, true) | |||||
{ | { | ||||
_gatewayClient = webSocketProvider(); | _gatewayClient = webSocketProvider(); | ||||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) | |||||
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) | |||||
_gatewayClient.BinaryMessage += async (data, index, count) => | _gatewayClient.BinaryMessage += async (data, index, count) => | ||||
{ | { | ||||
using (var compressed = new MemoryStream(data, index + 2, count - 2)) | using (var compressed = new MemoryStream(data, index + 2, count - 2)) | ||||
@@ -221,7 +221,5 @@ namespace Discord.WebSocket | |||||
remove { _recipientRemovedEvent.Remove(value); } | remove { _recipientRemovedEvent.Remove(value); } | ||||
} | } | ||||
private readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | private readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); | ||||
//TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; | |||||
} | } | ||||
} | } |
@@ -130,7 +130,7 @@ namespace Discord.WebSocket | |||||
protected override async Task OnLoginAsync(TokenType tokenType, string token) | protected override async Task OnLoginAsync(TokenType tokenType, string token) | ||||
{ | { | ||||
var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true}).ConfigureAwait(false); | |||||
var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); | |||||
_voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); | _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); | ||||
} | } | ||||
protected override async Task OnLogoutAsync() | protected override async Task OnLogoutAsync() | ||||