| @@ -32,26 +32,29 @@ namespace Discord.API | |||
| protected readonly JsonSerializer _serializer; | |||
| protected readonly SemaphoreSlim _stateLock; | |||
| private readonly RestClientProvider _restClientProvider; | |||
| private readonly string _userAgent; | |||
| protected string _authToken; | |||
| protected bool _isDisposed; | |||
| private CancellationTokenSource _loginCancelToken; | |||
| private IRestClient _restClient; | |||
| private bool _fetchCurrentUser; | |||
| public RetryMode DefaultRetryMode { get; } | |||
| public string UserAgent { get; } | |||
| public LoginState LoginState { get; private set; } | |||
| public TokenType AuthTokenType { get; private set; } | |||
| public User CurrentUser { 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; | |||
| _userAgent = userAgent; | |||
| UserAgent = userAgent; | |||
| _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; | |||
| RequestQueue = requestQueue; | |||
| FetchCurrentUser = true; | |||
| _fetchCurrentUser = fetchCurrentUser; | |||
| _stateLock = new SemaphoreSlim(1, 1); | |||
| @@ -61,7 +64,7 @@ namespace Discord.API | |||
| { | |||
| _restClient = _restClientProvider(baseUrl); | |||
| _restClient.SetHeader("accept", "*/*"); | |||
| _restClient.SetHeader("user-agent", _userAgent); | |||
| _restClient.SetHeader("user-agent", UserAgent); | |||
| _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | |||
| } | |||
| internal static string GetPrefixedToken(TokenType tokenType, string token) | |||
| @@ -120,8 +123,8 @@ namespace Discord.API | |||
| _authToken = token; | |||
| _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; | |||
| } | |||
| @@ -257,6 +260,8 @@ namespace Discord.API | |||
| { | |||
| if (!request.Options.IgnoreState) | |||
| CheckState(); | |||
| if (request.Options.RetryMode == null) | |||
| request.Options.RetryMode = DefaultRetryMode; | |||
| var stopwatch = Stopwatch.StartNew(); | |||
| var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); | |||
| @@ -19,6 +19,9 @@ namespace Discord | |||
| public const int MaxMessagesPerBatch = 100; | |||
| 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> | |||
| public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | |||
| } | |||
| @@ -12,6 +12,7 @@ namespace Discord | |||
| DateTimeOffset? JoinedAt { get; } | |||
| /// <summary> Gets the nickname for this user. </summary> | |||
| string Nickname { get; } | |||
| /// <summary> Gets the guild-level permissions for this user. </summary> | |||
| GuildPermissions GuildPermissions { get; } | |||
| /// <summary> Gets the guild for this user. </summary> | |||
| @@ -1,6 +1,8 @@ | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| #if DEBUG_LIMITS | |||
| using System.Diagnostics; | |||
| #endif | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Threading; | |||
| @@ -63,7 +65,11 @@ namespace Discord.Net.Queue | |||
| 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); | |||
| 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 System; | |||
| #if DEBUG_LIMITS | |||
| @@ -88,7 +87,10 @@ namespace Discord.Net.Queue | |||
| #if DEBUG_LIMITS | |||
| Debug.WriteLine($"[{id}] (!) 502"); | |||
| #endif | |||
| continue; //Continue | |||
| if ((request.Options.RetryMode & RetryMode.Retry502) == 0) | |||
| throw new HttpException(HttpStatusCode.BadGateway, null); | |||
| continue; //Retry | |||
| default: | |||
| string reason = null; | |||
| if (response.Stream != null) | |||
| @@ -115,13 +117,28 @@ namespace Discord.Net.Queue | |||
| return response.Stream; | |||
| } | |||
| } | |||
| catch (TimeoutException) | |||
| { | |||
| #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"); | |||
| throw; | |||
| } | |||
| #endif | |||
| if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) | |||
| throw; | |||
| await Task.Delay(500); | |||
| continue; //Retry | |||
| } | |||
| finally | |||
| { | |||
| UpdateRateLimit(id, request, info, lag, false); | |||
| @@ -140,7 +157,7 @@ namespace Discord.Net.Queue | |||
| while (true) | |||
| { | |||
| if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested) | |||
| if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) | |||
| { | |||
| if (!isRateLimited) | |||
| throw new TimeoutException(); | |||
| @@ -162,6 +179,10 @@ namespace Discord.Net.Queue | |||
| isRateLimited = true; | |||
| await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); | |||
| } | |||
| if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | |||
| throw new RateLimitedException(); | |||
| if (resetAt.HasValue) | |||
| { | |||
| if (resetAt > timeoutAt) | |||
| @@ -171,7 +192,7 @@ namespace Discord.Net.Queue | |||
| Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); | |||
| #endif | |||
| if (millis > 0) | |||
| await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); | |||
| await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| @@ -180,7 +201,7 @@ namespace Discord.Net.Queue | |||
| #if DEBUG_LIMITS | |||
| Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); | |||
| #endif | |||
| await Task.Delay(500, request.CancelToken).ConfigureAwait(false); | |||
| await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); | |||
| } | |||
| continue; | |||
| } | |||
| @@ -15,7 +15,7 @@ namespace Discord.Net.Queue | |||
| 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() | |||
| { | |||
| 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 System; | |||
| using System.IO; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Net.Queue | |||
| @@ -14,7 +13,6 @@ namespace Discord.Net.Queue | |||
| public DateTimeOffset? TimeoutAt { get; } | |||
| public TaskCompletionSource<Stream> Promise { get; } | |||
| public RequestOptions Options { get; } | |||
| public CancellationToken CancelToken { get; internal set; } | |||
| public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) | |||
| { | |||
| @@ -24,14 +22,13 @@ namespace Discord.Net.Queue | |||
| Method = method; | |||
| Endpoint = endpoint; | |||
| Options = options; | |||
| CancelToken = CancellationToken.None; | |||
| TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; | |||
| Promise = new TaskCompletionSource<Stream>(); | |||
| } | |||
| 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 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 CancellationToken CancelToken { get; set; } = CancellationToken.None; | |||
| public RetryMode? RetryMode { get; set; } | |||
| public bool HeaderOnly { get; internal set; } | |||
| internal bool IgnoreState { get; set; } | |||
| @@ -13,7 +20,7 @@ | |||
| internal bool IsClientBucket { get; set; } | |||
| internal static RequestOptions CreateOrClone(RequestOptions options) | |||
| { | |||
| { | |||
| if (options == null) | |||
| return new RequestOptions(); | |||
| 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 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); | |||
| _clientId = clientId; | |||
| _origin = origin; | |||
| FetchCurrentUser = false; | |||
| _requestQueue = requestQueue ?? new RequestQueue(); | |||
| _requests = new ConcurrentDictionary<Guid, RpcRequest>(); | |||
| @@ -32,11 +32,12 @@ namespace Discord.API | |||
| 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.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) => | |||
| { | |||
| using (var compressed = new MemoryStream(data, index + 2, count - 2)) | |||
| @@ -221,7 +221,5 @@ namespace Discord.WebSocket | |||
| remove { _recipientRemovedEvent.Remove(value); } | |||
| } | |||
| 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) | |||
| { | |||
| 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); | |||
| } | |||
| protected override async Task OnLogoutAsync() | |||