From 66097b3fd77a163bf99df62fcc9984214d598bc7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 09:25:38 -0300 Subject: [PATCH 01/13] Added MessageQueue --- src/Discord.Net/API/DiscordRawClient.cs | 198 ++++++++-------- src/Discord.Net/Discord.Net.csproj | 9 +- src/Discord.Net/Logging/ILogger.cs | 1 + src/Discord.Net/Logging/LogManager.cs | 40 ++-- src/Discord.Net/Net/RateLimitException.cs | 15 ++ src/Discord.Net/Net/Rest/DefaultRestClient.cs | 5 + src/Discord.Net/Net/Rest/IMessageQueue.cs | 7 - .../Net/Rest/RequestQueue/BucketGroup.cs | 8 + .../Net/Rest/RequestQueue/GlobalBucket.cs | 8 + .../Net/Rest/RequestQueue/GuildBucket.cs | 10 + .../Net/Rest/RequestQueue/IRequestQueue.cs | 10 + .../Net/Rest/RequestQueue/RequestQueue.cs | 163 +++++++++++++ .../Rest/RequestQueue/RequestQueueBucket.cs | 223 ++++++++++++++++++ .../Net/Rest/RequestQueue/RestRequest.cs | 38 +++ src/Discord.Net/Rest/DiscordClient.cs | 2 +- .../Rest/Entities/Channels/TextChannel.cs | 8 +- src/Discord.Net/Rest/Entities/Message.cs | 8 +- 17 files changed, 618 insertions(+), 135 deletions(-) create mode 100644 src/Discord.Net/Net/RateLimitException.cs delete mode 100644 src/Discord.Net/Net/Rest/IMessageQueue.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs create mode 100644 src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index 575ece0c8..49cc122f4 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -21,6 +21,7 @@ namespace Discord.API { internal event EventHandler SentRequest; + private readonly RequestQueue _requestQueue; private readonly IRestClient _restClient; private readonly CancellationToken _cancelToken; private readonly JsonSerializer _serializer; @@ -46,6 +47,7 @@ namespace Discord.API _restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken); _restClient.SetHeader("authorization", authToken); _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); + _requestQueue = new RequestQueue(_restClient); _serializer = new JsonSerializer(); _serializer.Converters.Add(new ChannelTypeConverter()); @@ -60,113 +62,73 @@ namespace Discord.API } //Core - public async Task Send(string method, string endpoint) + public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) + => SendInternal(method, endpoint, null, bucket); + public Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) + => SendInternal(method, endpoint, payload, bucket); + public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) + => SendInternal(method, endpoint, multipartArgs, bucket); + public async Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - { - var stopwatch = Stopwatch.StartNew(); - Stream responseStream; - try - { - responseStream = await _restClient.Send(method, endpoint, (string)null).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!HandleException(ex)) - throw; - return null; - } - int bytes = (int)responseStream.Length; - stopwatch.Stop(); - var response = Deserialize(responseStream); - - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + => Deserialize(await SendInternal(method, endpoint, null, bucket).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) + where TResponse : class + => Deserialize(await SendInternal(method, endpoint, payload, bucket).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) + where TResponse : class + => Deserialize(await SendInternal(method, endpoint, multipartArgs, bucket).ConfigureAwait(false)); + + public Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, null, bucket, guildId); + public Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, payload, bucket, guildId); + public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, multipartArgs, bucket, guildId); + public async Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) + where TResponse : class + => Deserialize(await SendInternal(method, endpoint, null, bucket, guildId).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) + where TResponse : class + => Deserialize(await SendInternal(method, endpoint, payload, bucket, guildId).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) + where TResponse : class + => Deserialize(await SendInternal(method, endpoint, multipartArgs, bucket, guildId).ConfigureAwait(false)); - return response; - } - public async Task Send(string method, string endpoint) - { - var stopwatch = Stopwatch.StartNew(); - try - { - await _restClient.Send(method, endpoint, (string)null).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!HandleException(ex)) - throw; - return; - } - stopwatch.Stop(); + private Task SendInternal(string method, string endpoint, object payload, GlobalBucket bucket) + => SendInternal(method, endpoint, payload, BucketGroup.Global, (int)bucket, 0); + private Task SendInternal(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, payload, BucketGroup.Guild, (int)bucket, guildId); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, GlobalBucket bucket) + => SendInternal(method, endpoint, multipartArgs, BucketGroup.Global, (int)bucket, 0); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, multipartArgs, BucketGroup.Guild, (int)bucket, guildId); - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); - } - public async Task Send(string method, string endpoint, object payload) - where TResponse : class + private async Task SendInternal(string method, string endpoint, object payload, BucketGroup group, int bucketId, ulong guildId) { - string requestStream = Serialize(payload); var stopwatch = Stopwatch.StartNew(); - Stream responseStream; - try - { - responseStream = await _restClient.Send(method, endpoint, requestStream).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!HandleException(ex)) - throw; - return null; - } + string json = null; + if (payload != null) + json = Serialize(payload); + var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json), group, bucketId, guildId).ConfigureAwait(false); int bytes = (int)responseStream.Length; stopwatch.Stop(); - var response = Deserialize(responseStream); double milliseconds = ToMilliseconds(stopwatch); SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); - return response; + return responseStream; } - public async Task Send(string method, string endpoint, object payload) + private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, BucketGroup group, int bucketId, ulong guildId) { - string requestStream = Serialize(payload); var stopwatch = Stopwatch.StartNew(); - try - { - await _restClient.Send(method, endpoint, requestStream).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!HandleException(ex)) - throw; - return; - } - stopwatch.Stop(); - - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); - } - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs) - where TResponse : class - { - var stopwatch = Stopwatch.StartNew(); - var responseStream = await _restClient.Send(method, endpoint).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs), group, bucketId, guildId).ConfigureAwait(false); + int bytes = (int)responseStream.Length; stopwatch.Stop(); - var response = Deserialize(responseStream); double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, (int)responseStream.Length, milliseconds)); - - return response; - } - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs) - { - var stopwatch = Stopwatch.StartNew(); - await _restClient.Send(method, endpoint).ConfigureAwait(false); - stopwatch.Stop(); + SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); + return responseStream; } //Gateway @@ -623,29 +585,50 @@ namespace Discord.API else return result[0]; } - public async Task CreateMessage(ulong channelId, CreateMessageParams args) + public Task CreateMessage(ulong channelId, CreateMessageParams args) + => CreateMessage(0, channelId, args); + public async Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args) { if (args == null) throw new ArgumentNullException(nameof(args)); if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - return await Send("POST", $"channels/{channelId}/messages", args).ConfigureAwait(false); + if (guildId != 0) + return await Send("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + else + return await Send("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage).ConfigureAwait(false); } - public async Task UploadFile(ulong channelId, Stream file, UploadFileParams args) + public Task UploadFile(ulong channelId, Stream file, UploadFileParams args) + => UploadFile(0, channelId, file, args); + public async Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args) { if (args == null) throw new ArgumentNullException(nameof(args)); + //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary()).ConfigureAwait(false); + if (guildId != 0) + return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + else + return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary()).ConfigureAwait(false); } - public async Task DeleteMessage(ulong channelId, ulong messageId) + public Task DeleteMessage(ulong channelId, ulong messageId) + => DeleteMessage(0, channelId, messageId); + public async Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId) { + //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); - await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false); + if (guildId != 0) + await Send("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId).ConfigureAwait(false); + else + await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false); } - public async Task DeleteMessages(ulong channelId, DeleteMessagesParam args) + public Task DeleteMessages(ulong channelId, DeleteMessagesParam args) + => DeleteMessages(0, channelId, args); + public async Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParam args) { + //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); if (args == null) throw new ArgumentNullException(nameof(args)); if (args.MessageIds == null) throw new ArgumentNullException(nameof(args.MessageIds)); @@ -655,20 +638,29 @@ namespace Discord.API case 0: throw new ArgumentOutOfRangeException(nameof(args.MessageIds)); case 1: - await DeleteMessage(channelId, messageIds[0]).ConfigureAwait(false); + await DeleteMessage(guildId, channelId, messageIds[0]).ConfigureAwait(false); break; default: - await Send("POST", $"channels/{channelId}/messages/bulk_delete", args).ConfigureAwait(false); + if (guildId != 0) + await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId).ConfigureAwait(false); + else + await Send("POST", $"channels/{channelId}/messages/bulk_delete", args).ConfigureAwait(false); break; } } - public async Task ModifyMessage(ulong channelId, ulong messageId, ModifyMessageParams args) + public Task ModifyMessage(ulong channelId, ulong messageId, ModifyMessageParams args) + => ModifyMessage(0, channelId, messageId, args); + public async Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) { if (args == null) throw new ArgumentNullException(nameof(args)); + //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); - return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args).ConfigureAwait(false); + if (guildId != 0) + return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + else + return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args).ConfigureAwait(false); } public async Task AckMessage(ulong channelId, ulong messageId) { @@ -775,11 +767,5 @@ namespace Discord.API using (JsonReader reader = new JsonTextReader(text)) return _serializer.Deserialize(reader); } - - private bool HandleException(Exception ex) - { - //TODO: Implement... maybe via SentRequest? Need to bubble this up to DiscordClient or a MessageQueue - return false; - } } } diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index a178e7f8e..f7c9e7a8c 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -95,6 +95,13 @@ + + + + + + + @@ -181,7 +188,7 @@ - + diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs index 787965786..f8679d0ec 100644 --- a/src/Discord.Net/Logging/ILogger.cs +++ b/src/Discord.Net/Logging/ILogger.cs @@ -8,6 +8,7 @@ namespace Discord.Logging void Log(LogSeverity severity, string message, Exception exception = null); void Log(LogSeverity severity, FormattableString message, Exception exception = null); + void Log(LogSeverity severity, Exception exception); void Error(string message, Exception exception = null); void Error(FormattableString message, Exception exception = null); diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 5b1e4d14b..0c183071d 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -23,6 +23,11 @@ namespace Discord.Logging if (severity <= Level) Message(this, new LogMessageEventArgs(severity, source, message.ToString(), ex)); } + public void Log(LogSeverity severity, string source, Exception ex) + { + if (severity <= Level) + Message(this, new LogMessageEventArgs(severity, source, null, ex)); + } void ILogger.Log(LogSeverity severity, string message, Exception ex) { if (severity <= Level) @@ -33,71 +38,76 @@ namespace Discord.Logging if (severity <= Level) Message(this, new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)); } + void ILogger.Log(LogSeverity severity, Exception ex) + { + if (severity <= Level) + Message(this, new LogMessageEventArgs(severity, "Discord", null, ex)); + } public void Error(string source, string message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); public void Error(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); - public void Error(string source, Exception ex = null) - => Log(LogSeverity.Error, source, (string)null, ex); + public void Error(string source, Exception ex) + => Log(LogSeverity.Error, source, ex); void ILogger.Error(string message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); void ILogger.Error(FormattableString message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); void ILogger.Error(Exception ex) - => Log(LogSeverity.Error, "Discord", (string)null, ex); + => Log(LogSeverity.Error, "Discord", ex); public void Warning(string source, string message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); public void Warning(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); - public void Warning(string source, Exception ex = null) - => Log(LogSeverity.Warning, source, (string)null, ex); + public void Warning(string source, Exception ex) + => Log(LogSeverity.Warning, source, ex); void ILogger.Warning(string message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); void ILogger.Warning(FormattableString message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); void ILogger.Warning(Exception ex) - => Log(LogSeverity.Warning, "Discord", (string)null, ex); + => Log(LogSeverity.Warning, "Discord", ex); public void Info(string source, string message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); public void Info(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); - public void Info(string source, Exception ex = null) - => Log(LogSeverity.Info, source, (string)null, ex); + public void Info(string source, Exception ex) + => Log(LogSeverity.Info, source, ex); void ILogger.Info(string message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); void ILogger.Info(FormattableString message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); void ILogger.Info(Exception ex) - => Log(LogSeverity.Info, "Discord", (string)null, ex); + => Log(LogSeverity.Info, "Discord", ex); public void Verbose(string source, string message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); public void Verbose(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); - public void Verbose(string source, Exception ex = null) - => Log(LogSeverity.Verbose, source, (string)null, ex); + public void Verbose(string source, Exception ex) + => Log(LogSeverity.Verbose, source, ex); void ILogger.Verbose(string message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); void ILogger.Verbose(FormattableString message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); void ILogger.Verbose(Exception ex) - => Log(LogSeverity.Verbose, "Discord", (string)null, ex); + => Log(LogSeverity.Verbose, "Discord", ex); public void Debug(string source, string message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); public void Debug(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, Exception ex = null) - => Log(LogSeverity.Debug, source, (string)null, ex); + public void Debug(string source, Exception ex) + => Log(LogSeverity.Debug, source, ex); void ILogger.Debug(string message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); void ILogger.Debug(FormattableString message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); void ILogger.Debug(Exception ex) - => Log(LogSeverity.Debug, "Discord", (string)null, ex); + => Log(LogSeverity.Debug, "Discord", ex); internal Logger CreateLogger(string name) => new Logger(this, name); } diff --git a/src/Discord.Net/Net/RateLimitException.cs b/src/Discord.Net/Net/RateLimitException.cs new file mode 100644 index 000000000..a07e90760 --- /dev/null +++ b/src/Discord.Net/Net/RateLimitException.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace Discord.Net +{ + public class HttpRateLimitException : HttpException + { + public int RetryAfterMilliseconds { get; } + + public HttpRateLimitException(int retryAfterMilliseconds) + : base((HttpStatusCode)429) + { + RetryAfterMilliseconds = retryAfterMilliseconds; + } + } +} diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 3c72f7258..0aec73597 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -3,6 +3,7 @@ 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; @@ -118,7 +119,11 @@ namespace Discord.Net.Rest int statusCode = (int)response.StatusCode; if (statusCode < 200 || statusCode >= 300) //2xx = Success + { + if (statusCode == 429) + throw new HttpRateLimitException(int.Parse(response.Headers.GetValues("retry-after").First())); throw new HttpException(response.StatusCode); + } return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } diff --git a/src/Discord.Net/Net/Rest/IMessageQueue.cs b/src/Discord.Net/Net/Rest/IMessageQueue.cs deleted file mode 100644 index a61131ed8..000000000 --- a/src/Discord.Net/Net/Rest/IMessageQueue.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Net.Rest -{ - public interface IMessageQueue - { - int Count { get; } - } -} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs b/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs new file mode 100644 index 000000000..54c3e717d --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs @@ -0,0 +1,8 @@ +namespace Discord.Net.Rest +{ + internal enum BucketGroup + { + Global, + Guild + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs new file mode 100644 index 000000000..4e7126f5e --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs @@ -0,0 +1,8 @@ +namespace Discord.Net.Rest +{ + public enum GlobalBucket + { + General, + DirectMessage + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs new file mode 100644 index 000000000..ccb3fa994 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs @@ -0,0 +1,10 @@ +namespace Discord.Net.Rest +{ + public enum GuildBucket + { + SendEditMessage, + DeleteMessage, + DeleteMessages, + Nickname + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs b/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs new file mode 100644 index 000000000..27231a334 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + public interface IRequestQueue + { + Task Clear(GlobalBucket type); + Task Clear(GuildBucket type, ulong guildId); + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs new file mode 100644 index 000000000..155b683e7 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + public class RequestQueue : IRequestQueue + { + private SemaphoreSlim _lock; + private RequestQueueBucket[] _globalBuckets; + private Dictionary[] _guildBuckets; + + public IRestClient RestClient { get; } + + public RequestQueue(IRestClient restClient) + { + RestClient = restClient; + + _lock = new SemaphoreSlim(1, 1); + _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; + _guildBuckets = new Dictionary[Enum.GetValues(typeof(GuildBucket)).Length]; + } + + internal async Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + { + RequestQueueBucket bucket; + + await Lock().ConfigureAwait(false); + try + { + bucket = GetBucket(group, bucketId, guildId); + bucket.Queue(request); + } + finally { Unlock(); } + + //There is a chance the bucket will send this request on its own, but this will simply become a noop then. + var _ = bucket.ProcessQueue(acquireLock: true).ConfigureAwait(false); + + return await request.Promise.Task.ConfigureAwait(false); + } + + private RequestQueueBucket CreateBucket(GlobalBucket bucket) + { + switch (bucket) + { + //Globals + case GlobalBucket.General: return new RequestQueueBucket(this, bucket, int.MaxValue, 0); //Catch-all + case GlobalBucket.DirectMessage: return new RequestQueueBucket(this, bucket, 5, 5); + + default: throw new ArgumentException($"Unknown global bucket: {bucket}", nameof(bucket)); + } + } + private RequestQueueBucket CreateBucket(GuildBucket bucket, ulong guildId) + { + switch (bucket) + { + //Per Guild + case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5); + case GuildBucket.DeleteMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 1); + case GuildBucket.DeleteMessages: return new RequestQueueBucket(this, bucket, guildId, 1, 1); + case GuildBucket.Nickname: return new RequestQueueBucket(this, bucket, guildId, 1, 1); + + default: throw new ArgumentException($"Unknown guild bucket: {bucket}", nameof(bucket)); + } + } + + private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong guildId) + { + switch (group) + { + case BucketGroup.Global: + return GetGlobalBucket((GlobalBucket)bucketId); + case BucketGroup.Guild: + return GetGuildBucket((GuildBucket)bucketId, guildId); + default: + throw new ArgumentException($"Unknown bucket group: {group}", nameof(group)); + } + } + private RequestQueueBucket GetGlobalBucket(GlobalBucket type) + { + var bucket = _globalBuckets[(int)type]; + if (bucket == null) + { + bucket = CreateBucket(type); + _globalBuckets[(int)type] = bucket; + } + return bucket; + } + private RequestQueueBucket GetGuildBucket(GuildBucket type, ulong guildId) + { + var bucketGroup = _guildBuckets[(int)type]; + if (bucketGroup == null) + { + bucketGroup = new Dictionary(); + _guildBuckets[(int)type] = bucketGroup; + } + RequestQueueBucket bucket; + if (!bucketGroup.TryGetValue(guildId, out bucket)) + { + bucket = CreateBucket(type, guildId); + bucketGroup[guildId] = bucket; + } + return bucket; + } + + internal void DestroyGlobalBucket(GlobalBucket type) + { + //Assume this object is locked + + _globalBuckets[(int)type] = null; + } + internal void DestroyGuildBucket(GuildBucket type, ulong guildId) + { + //Assume this object is locked + + var bucketGroup = _guildBuckets[(int)type]; + if (bucketGroup != null) + bucketGroup.Remove(guildId); + } + + public async Task Lock() + { + await _lock.WaitAsync(); + } + public void Unlock() + { + _lock.Release(); + } + + public async Task Clear(GlobalBucket type) + { + var bucket = _globalBuckets[(int)type]; + if (bucket != null) + { + try + { + await bucket.Lock(); + bucket.Clear(); + } + finally { bucket.Unlock(); } + } + } + public async Task Clear(GuildBucket type, ulong guildId) + { + var bucketGroup = _guildBuckets[(int)type]; + if (bucketGroup != null) + { + RequestQueueBucket bucket; + if (bucketGroup.TryGetValue(guildId, out bucket)) + { + try + { + await bucket.Lock(); + bucket.Clear(); + } + finally { bucket.Unlock(); } + } + } + } + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs new file mode 100644 index 000000000..a3788a5b0 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + internal class RequestQueueBucket + { + private readonly RequestQueue _parent; + private readonly BucketGroup _bucketGroup; + private readonly int _bucketId; + private readonly ulong _guildId; + private readonly ConcurrentQueue _queue; + private readonly SemaphoreSlim _lock; + private Task _resetTask; + private DateTime? _retryAfter; + private bool _waitingToProcess; + + public int WindowMaxCount { get; } + public int WindowSeconds { get; } + public int WindowCount { get; private set; } + + public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds) + : this(parent, windowMaxCount, windowSeconds) + { + _bucketGroup = BucketGroup.Global; + _bucketId = (int)bucket; + _guildId = 0; + } + public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds) + : this(parent, windowMaxCount, windowSeconds) + { + _bucketGroup = BucketGroup.Guild; + _bucketId = (int)bucket; + _guildId = guildId; + } + private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds) + { + _parent = parent; + WindowMaxCount = windowMaxCount; + WindowSeconds = windowSeconds; + _queue = new ConcurrentQueue(); + _lock = new SemaphoreSlim(1, 1); + } + + public void Queue(RestRequest request) + { + //Assume this obj's parent is under lock + + _queue.Enqueue(request); + Debug($"Request queued ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); + } + public async Task ProcessQueue(bool acquireLock = false) + { + //Assume this obj is under lock + + int nextRetry = 1000; + + //If we have another ProcessQueue waiting to run, dont bother with this one + if (_waitingToProcess) return; + _waitingToProcess = true; + + if (acquireLock) + await Lock().ConfigureAwait(false); + try + { + _waitingToProcess = false; + while (true) + { + RestRequest request; + + //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort + if (WindowCount == WindowMaxCount) return; + //Get next request, return if queue is empty + if (!_queue.TryPeek(out request)) return; + + try + { + Stream stream; + if (request.IsMultipart) + stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams).ConfigureAwait(false); + else + stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json).ConfigureAwait(false); + request.Promise.SetResult(stream); + } + catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own + { + if (_resetTask == null) + { + //No reset has been queued yet, lets create one as if this *was* preemptive + _resetTask = ResetAfter(ex.RetryAfterMilliseconds); + Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); + } + else + { + //A preemptive reset is already queued, set RetryAfter to extend it + _retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); + Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); + } + return; + } + catch (HttpException ex) + { + if (ex.StatusCode == HttpStatusCode.BadGateway) //Gateway unavailable, retry + { + await Task.Delay(nextRetry).ConfigureAwait(false); + nextRetry *= 2; + if (nextRetry > 30000) + nextRetry = 30000; + continue; + } + else + { + //We dont need to throw this here, pass the exception via the promise + request.Promise.SetException(ex); + } + } + + //Request completed or had an error other than 429 + _queue.TryDequeue(out request); + WindowCount++; + nextRetry = 1000; + Debug($"Request succeeded ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); + + if (WindowCount == 1 && WindowSeconds > 0) + { + //First request for this window, schedule a reset + _resetTask = ResetAfter(WindowSeconds * 1000); + Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms"); + } + } + } + finally + { + if (acquireLock) + Unlock(); + } + } + public void Clear() + { + //Assume this obj is under lock + RestRequest request; + + while (_queue.TryDequeue(out request)) { } + } + + private async Task ResetAfter(int milliseconds) + { + if (milliseconds > 0) + await Task.Delay(milliseconds).ConfigureAwait(false); + try + { + await Lock().ConfigureAwait(false); + + //If an extension has been planned, start a new wait task + if (_retryAfter != null) + { + _resetTask = ResetAfter((int)(_retryAfter.Value - DateTime.UtcNow).TotalMilliseconds); + _retryAfter = null; + return; + } + + Debug($"Reset"); + //Reset the current window count and set our state back to normal + WindowCount = 0; + _resetTask = null; + + //Wait is over, work through the current queue + await ProcessQueue().ConfigureAwait(false); + + //If queue is empty and non-global, remove this bucket + if (_bucketGroup == BucketGroup.Guild && _queue.IsEmpty) + { + try + { + await _parent.Lock().ConfigureAwait(false); + if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks + _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); + } + finally + { + _parent.Unlock(); + } + } + } + finally + { + Unlock(); + } + } + + public async Task Lock() + { + await _lock.WaitAsync(); + } + public void Unlock() + { + _lock.Release(); + } + + //TODO: Remove + private void Debug(string text) + { + string name; + switch (_bucketGroup) + { + case BucketGroup.Global: + name = ((GlobalBucket)_bucketId).ToString(); + break; + case BucketGroup.Guild: + name = ((GuildBucket)_bucketId).ToString(); + break; + default: + name = "Unknown"; + break; + } + System.Diagnostics.Debug.WriteLine($"[{name}] {text}"); + } + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs new file mode 100644 index 000000000..86c7ca962 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + internal struct RestRequest + { + public string Method { get; } + public string Endpoint { get; } + public string Json { get; } + public IReadOnlyDictionary MultipartParams { get; } + public TaskCompletionSource Promise { get; } + + public bool IsMultipart => MultipartParams != null; + + public RestRequest(string method, string endpoint, string json) + : this(method, endpoint) + { + Json = json; + } + + public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams) + : this(method, endpoint) + { + MultipartParams = multipartParams; + } + + private RestRequest(string method, string endpoint) + { + Method = method; + Endpoint = endpoint; + Json = null; + MultipartParams = null; + Promise = new TaskCompletionSource(); + } + } +} diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 5b0e0d119..ac1bc0762 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -60,7 +60,7 @@ namespace Discord.Rest var cancelTokenSource = new CancellationTokenSource(); BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token, tokenType, token); - BaseClient.SentRequest += (s, e) => _log.Verbose($"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); //MessageQueue = new MessageQueue(RestClient, _restLogger); //await MessageQueue.Start(_cancelTokenSource.Token).ConfigureAwait(false); diff --git a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs index 24ce2d7e9..e15d7578a 100644 --- a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs @@ -64,7 +64,7 @@ namespace Discord.Rest public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.CreateMessage(Id, args).ConfigureAwait(false); + var model = await Discord.BaseClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); return new Message(this, model); } /// @@ -74,7 +74,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.UploadFile(Id, file, args).ConfigureAwait(false); + var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); return new Message(this, model); } } @@ -82,14 +82,14 @@ namespace Discord.Rest public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.UploadFile(Id, stream, args).ConfigureAwait(false); + var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); return new Message(this, model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.BaseClient.DeleteMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.BaseClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index 43abbebc7..4f232bed6 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -121,7 +121,13 @@ namespace Discord.Rest var args = new ModifyMessageParams(); func(args); - var model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false); + var guildChannel = Channel as GuildChannel; + + Model model; + if (guildChannel != null) + model = await Discord.BaseClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); + else + model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false); Update(model); } From 7cccf6b30c7f23aac9be28704bffcb2606ecf626 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 10:07:04 -0300 Subject: [PATCH 02/13] Fixed MessageQueue race condition --- .../Rest/RequestQueue/RequestQueueBucket.cs | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs index a3788a5b0..4e8a3c520 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs @@ -16,8 +16,8 @@ namespace Discord.Net.Rest private readonly ConcurrentQueue _queue; private readonly SemaphoreSlim _lock; private Task _resetTask; - private DateTime? _retryAfter; - private bool _waitingToProcess; + private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed + private int _id; public int WindowMaxCount { get; } public int WindowSeconds { get; } @@ -44,10 +44,12 @@ namespace Discord.Net.Rest WindowSeconds = windowSeconds; _queue = new ConcurrentQueue(); _lock = new SemaphoreSlim(1, 1); + _id = new System.Random().Next(0, int.MaxValue); } public void Queue(RestRequest request) { + if (_destroyed) throw new Exception(); //Assume this obj's parent is under lock _queue.Enqueue(request); @@ -75,7 +77,7 @@ namespace Discord.Net.Rest //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort if (WindowCount == WindowMaxCount) return; //Get next request, return if queue is empty - if (!_queue.TryPeek(out request)) return; + if (!_queue.TryPeek(out request)) break; try { @@ -88,17 +90,20 @@ namespace Discord.Net.Rest } catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own { - if (_resetTask == null) + WindowCount = WindowMaxCount; + var task = _resetTask; + if (task != null) { - //No reset has been queued yet, lets create one as if this *was* preemptive - _resetTask = ResetAfter(ex.RetryAfterMilliseconds); - Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); + Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); + var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); + await task.ConfigureAwait(false); + int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); + _resetTask = ResetAfter(millis); } else { - //A preemptive reset is already queued, set RetryAfter to extend it - _retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); - Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); + Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); + _resetTask = ResetAfter(ex.RetryAfterMilliseconds); } return; } @@ -132,6 +137,25 @@ namespace Discord.Net.Rest Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms"); } } + + //If queue is empty, non-global, and there is no active rate limit, remove this bucket + if (_resetTask == null && _bucketGroup == BucketGroup.Guild) + { + try + { + await _parent.Lock().ConfigureAwait(false); + if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks + { + Debug($"Destroy"); + _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); + _destroyed = true; + } + } + finally + { + _parent.Unlock(); + } + } } finally { @@ -155,36 +179,14 @@ namespace Discord.Net.Rest { await Lock().ConfigureAwait(false); - //If an extension has been planned, start a new wait task - if (_retryAfter != null) - { - _resetTask = ResetAfter((int)(_retryAfter.Value - DateTime.UtcNow).TotalMilliseconds); - _retryAfter = null; - return; - } - Debug($"Reset"); + //Reset the current window count and set our state back to normal WindowCount = 0; _resetTask = null; //Wait is over, work through the current queue await ProcessQueue().ConfigureAwait(false); - - //If queue is empty and non-global, remove this bucket - if (_bucketGroup == BucketGroup.Guild && _queue.IsEmpty) - { - try - { - await _parent.Lock().ConfigureAwait(false); - if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks - _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); - } - finally - { - _parent.Unlock(); - } - } } finally { @@ -217,7 +219,7 @@ namespace Discord.Net.Rest name = "Unknown"; break; } - System.Diagnostics.Debug.WriteLine($"[{name}] {text}"); + System.Diagnostics.Debug.WriteLine($"[{name} {_id}] {text}"); } } } From 0eb9ff6bd32e8f7a7b8632fd0cc1001d2a059463 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 10:27:58 -0300 Subject: [PATCH 03/13] Fixed several model processing bugs --- src/Discord.Net/API/DiscordRawClient.cs | 18 +++++++++++------- src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 4 ++-- src/Discord.Net/Rest/Entities/Message.cs | 8 ++++---- .../Rest/Entities/Users/GuildUser.cs | 4 ++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index 49cc122f4..a251af8c5 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -219,7 +219,7 @@ namespace Discord.API switch (channels.Length) { case 0: - throw new ArgumentOutOfRangeException(nameof(args)); + return; case 1: await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); break; @@ -476,8 +476,10 @@ namespace Discord.API if (result.Count > 1) return result.SelectMany(x => x); - else + else if (result.Count == 1) return result[0]; + else + return Array.Empty(); } public async Task RemoveGuildMember(ulong guildId, ulong userId) { @@ -535,7 +537,7 @@ namespace Discord.API switch (roles.Length) { case 0: - throw new ArgumentOutOfRangeException(nameof(args)); + return Array.Empty(); case 1: return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); default: @@ -580,10 +582,12 @@ namespace Discord.API if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } } - if (runs > 1) - return result.Take(runs).SelectMany(x => x); - else + if (i > 1) + return result.Take(i).SelectMany(x => x); + else if (i == 1) return result[0]; + else + return Array.Empty(); } public Task CreateMessage(ulong channelId, CreateMessageParams args) => CreateMessage(0, channelId, args); @@ -636,7 +640,7 @@ namespace Discord.API switch (messageIds.Length) { case 0: - throw new ArgumentOutOfRangeException(nameof(args.MessageIds)); + return; case 1: await DeleteMessage(guildId, channelId, messageIds[0]).ConfigureAwait(false); break; diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index bcc73e993..18a80655f 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -81,11 +81,11 @@ namespace Discord.Rest { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis[i] = new Emoji(model.Emojis[i]); + emojis.Add(new Emoji(model.Emojis[i])); Emojis = emojis.ToArray(); } else - Emojis = ImmutableArray.Empty; + Emojis = Array.Empty(); var roles = new ConcurrentDictionary(1, model.Roles?.Length ?? 0); if (model.Roles != null) diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index 4f232bed6..ebffae995 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -68,7 +68,7 @@ namespace Discord.Rest Attachments = ImmutableArray.Create(attachments); } else - Attachments = ImmutableArray.Empty; + Attachments = Array.Empty(); if (model.Embeds.Length > 0) { @@ -78,18 +78,18 @@ namespace Discord.Rest Embeds = ImmutableArray.Create(embeds); } else - Embeds = ImmutableArray.Empty; + Embeds = Array.Empty(); if (model.Mentions.Length > 0) { var discord = Discord; var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); for (int i = 0; i < model.Mentions.Length; i++) - builder[i] = new PublicUser(discord, model.Mentions[i]); + builder.Add(new PublicUser(discord, model.Mentions[i])); MentionedUsers = builder.ToArray(); } else - MentionedUsers = ImmutableArray.Empty; + MentionedUsers = Array.Empty(); MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content); MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content); if (model.IsMentioningEveryone) diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 1df857695..b62656dfc 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -39,9 +39,9 @@ namespace Discord.Rest Nickname = model.Nick; var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles[0] = Guild.EveryoneRole; + roles.Add(Guild.EveryoneRole); for (int i = 0; i < model.Roles.Length; i++) - roles[i + 1] = Guild.GetRole(model.Roles[i]); + roles.Add(Guild.GetRole(model.Roles[i])); _roles = roles.ToImmutable(); } From 4d565d1b29a15ffb10c269a68b4d5648bc584802 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 10:35:37 -0300 Subject: [PATCH 04/13] Added ToString to several Rest models --- src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs | 3 --- src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs | 2 ++ src/Discord.Net/Rest/Entities/Message.cs | 3 +++ src/Discord.Net/Rest/Entities/Users/Connection.cs | 2 ++ src/Discord.Net/Rest/Entities/Users/User.cs | 2 ++ 10 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs index 0361ec8c2..c14e918fe 100644 --- a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs @@ -153,9 +153,6 @@ namespace Discord.Rest Update(model); } - /// - public override string ToString() => Name ?? Id.ToString(); - IGuild IGuildChannel.Guild => Guild; async Task IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index 18a80655f..481a8d884 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -333,6 +333,8 @@ namespace Discord.Rest } } + public override string ToString() => Name ?? Id.ToString(); + IEnumerable IGuild.Emojis => Emojis; ulong IGuild.EveryoneRoleId => EveryoneRole.Id; IEnumerable IGuild.Features => Features; diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs index f3045e10d..5d9220ade 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs @@ -28,5 +28,7 @@ namespace Discord.Rest ChannelId = model.ChannelId; IsEnabled = model.Enabled; } + + public override string ToString() => $"{Id} ({(IsEnabled ? "Enabled" : "Disabled")})"; } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs index 35482d96d..c479f9f4d 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs @@ -77,6 +77,8 @@ namespace Discord.Rest await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } + public override string ToString() => $"{Name ?? Id.ToString()} ({(IsEnabled ? "Enabled" : "Disabled")})"; + IGuild IGuildIntegration.Guild => Guild; IRole IGuildIntegration.Role => Role; IUser IGuildIntegration.User => User; diff --git a/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs index ba5fb431f..f28061955 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs @@ -7,5 +7,7 @@ /// public string Name { get; private set; } + + public override string ToString() => Name ?? Id.ToString(); } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs index 7e4f41fbd..d278b6341 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs @@ -51,5 +51,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to leave a guild the current user owns, use Delete() instead."); await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); } + + public override string ToString() => Name ?? Id.ToString(); } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs index 557fca63d..1c3ee7f20 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs @@ -26,5 +26,7 @@ namespace Discord.Rest SampleHostname = model.SampleHostname; SamplePort = model.SamplePort; } + + public override string ToString() => $"{Name ?? Id.ToString()}"; } } diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index ebffae995..97ed04512 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -137,6 +137,9 @@ namespace Discord.Rest await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); } + + public override string ToString() => $"{Author.ToString()}: {Text}"; + IUser IMessage.Author => Author; IReadOnlyList IMessage.Attachments => Attachments; IReadOnlyList IMessage.Embeds => Embeds; diff --git a/src/Discord.Net/Rest/Entities/Users/Connection.cs b/src/Discord.Net/Rest/Entities/Users/Connection.cs index 6f243515a..9795dc207 100644 --- a/src/Discord.Net/Rest/Entities/Users/Connection.cs +++ b/src/Discord.Net/Rest/Entities/Users/Connection.cs @@ -23,5 +23,7 @@ namespace Discord.Rest Integrations = model.Integrations; } + + public override string ToString() => $"{Name ?? Id.ToString()} ({Type})"; } } diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs index df596360a..9572c6620 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Rest/Entities/Users/User.cs @@ -54,6 +54,8 @@ namespace Discord.Rest return new DMChannel(Discord, model); } + public override string ToString() => $"{Username ?? Id.ToString()}"; + /// string IUser.CurrentGame => null; /// From 227c8f3e60acfd330c12b3144a3131cfb018035a Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 10:38:49 -0300 Subject: [PATCH 05/13] Minor rename --- .../Rest/Entities/Guilds/{UserGUild.cs => UserGuild.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Discord.Net/Rest/Entities/Guilds/{UserGUild.cs => UserGuild.cs} (100%) diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs similarity index 100% rename from src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs rename to src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs From 4bc37d8c33571b6d2af041e81f2b5c05874fccbb Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 10:57:29 -0300 Subject: [PATCH 06/13] Added IGuild.IsOwner, cleaned a few exceptions --- src/Discord.Net/Common/Entities/Guilds/IGuild.cs | 2 ++ src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 6 ++++++ src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs index 2c3d8a3c8..16db2c0e3 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs @@ -11,6 +11,8 @@ namespace Discord int AFKTimeout { get; } /// Returns true if this guild is embeddable (e.g. widget) bool IsEmbeddable { get; } + /// Returns true if the current user owns this guild. + bool IsOwner { get; } /// Gets the name of this guild. string Name { get; } int VerificationLevel { get; } diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index 481a8d884..60170d0fa 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -46,6 +46,8 @@ namespace Discord.Rest /// public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); /// + public bool IsOwner => OwnerId == Discord.CurrentUser.Id; + /// public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); /// public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); @@ -155,11 +157,15 @@ namespace Discord.Rest /// public async Task Leave() { + if (IsOwner) + throw new InvalidOperationException("Unable to leave a guild the current user owns."); await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { + if (!IsOwner) + throw new InvalidOperationException("Unable to delete a guild the current user does not own."); await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); } diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs index d278b6341..cae71f5ae 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs @@ -41,14 +41,14 @@ namespace Discord.Rest public async Task Leave() { if (IsOwner) - throw new InvalidOperationException("Unable to leave a guild the current user owns, use Delete() instead."); + throw new InvalidOperationException("Unable to leave a guild the current user owns."); await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { if (!IsOwner) - throw new InvalidOperationException("Unable to leave a guild the current user owns, use Delete() instead."); + throw new InvalidOperationException("Unable to delete a guild the current user does not own."); await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); } From 14bb0f7e2e1dc4d2104a2a925a0bc0634258c8e5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 23:18:01 -0300 Subject: [PATCH 07/13] Added support for optional model parameters, fixed a few model errors --- src/Discord.Net/API/DiscordRawClient.cs | 63 ++++++++++--------- src/Discord.Net/API/IOptional.cs | 8 +++ src/Discord.Net/API/Optional.cs | 26 ++++++++ .../API/Rest/CreateChannelInviteParams.cs | 8 +-- .../API/Rest/CreateGuildBanParams.cs | 2 +- .../API/Rest/CreateGuildChannelParams.cs | 3 +- src/Discord.Net/API/Rest/CreateGuildParams.cs | 3 +- .../API/Rest/CreateMessageParams.cs | 5 +- .../API/Rest/GetChannelMessagesParams.cs | 3 +- .../API/Rest/GetGuildMembersParams.cs | 4 +- .../Rest/ModifyChannelPermissionsParams.cs | 4 +- .../API/Rest/ModifyCurrentUserParams.cs | 10 +-- .../API/Rest/ModifyGuildChannelParams.cs | 4 +- .../API/Rest/ModifyGuildChannelsParams.cs | 4 +- .../API/Rest/ModifyGuildEmbedParams.cs | 6 +- .../API/Rest/ModifyGuildIntegrationParams.cs | 6 +- .../API/Rest/ModifyGuildMemberParams.cs | 15 ++--- src/Discord.Net/API/Rest/ModifyGuildParams.cs | 18 +++--- .../API/Rest/ModifyGuildRoleParams.cs | 10 +-- .../API/Rest/ModifyGuildRolesParams.cs | 2 +- .../API/Rest/ModifyMessageParams.cs | 2 +- .../API/Rest/ModifyTextChannelParams.cs | 2 +- .../API/Rest/ModifyVoiceChannelParams.cs | 2 +- src/Discord.Net/API/Rest/UploadFileParams.cs | 23 +++---- .../Common/Entities/Guilds/IGuild.cs | 4 +- .../Common/Entities/Users/IUser.cs | 2 + src/Discord.Net/Discord.Net.csproj | 4 ++ .../Net/Converters/ImageConverter.cs | 7 ++- .../Converters/OptionalContractResolver.cs | 34 ++++++++++ .../Net/Converters/OptionalConverter.cs | 23 +++++++ src/Discord.Net/Net/Rest/DefaultRestClient.cs | 20 +++--- src/Discord.Net/Net/Rest/IRestClient.cs | 4 +- .../Rest/RequestQueue/RequestQueueBucket.cs | 4 +- .../Net/Rest/RequestQueue/RestRequest.cs | 12 ++-- src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 9 ++- .../Rest/Entities/Users/GuildUser.cs | 12 +++- src/Discord.Net/Rest/Entities/Users/User.cs | 2 + 37 files changed, 251 insertions(+), 119 deletions(-) create mode 100644 src/Discord.Net/API/IOptional.cs create mode 100644 src/Discord.Net/API/Optional.cs create mode 100644 src/Discord.Net/Net/Converters/OptionalContractResolver.cs create mode 100644 src/Discord.Net/Net/Converters/OptionalConverter.cs diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index a251af8c5..effc5fde2 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -45,11 +45,13 @@ namespace Discord.API } _restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken); + _restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("authorization", authToken); _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); _requestQueue = new RequestQueue(_restClient); _serializer = new JsonSerializer(); + _serializer.Converters.Add(new OptionalConverter()); _serializer.Converters.Add(new ChannelTypeConverter()); _serializer.Converters.Add(new ImageConverter()); _serializer.Converters.Add(new NullableUInt64Converter()); @@ -59,58 +61,59 @@ namespace Discord.API _serializer.Converters.Add(new UInt64Converter()); _serializer.Converters.Add(new UInt64EntityConverter()); _serializer.Converters.Add(new UserStatusConverter()); + _serializer.ContractResolver = new OptionalContractResolver(); } //Core public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, null, bucket); + => SendInternal(method, endpoint, null, true, bucket); public Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, payload, bucket); + => SendInternal(method, endpoint, payload, true, bucket); public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, multipartArgs, bucket); + => SendInternal(method, endpoint, multipartArgs, true, bucket); public async Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, bucket).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, bucket).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, bucket).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); public Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, null, bucket, guildId); + => SendInternal(method, endpoint, null, true, bucket, guildId); public Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, payload, bucket, guildId); + => SendInternal(method, endpoint, payload, true, bucket, guildId); public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, multipartArgs, bucket, guildId); + => SendInternal(method, endpoint, multipartArgs, true, bucket, guildId); public async Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, bucket, guildId).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); public async Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, bucket, guildId).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, bucket, guildId).ConfigureAwait(false)); + => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); - private Task SendInternal(string method, string endpoint, object payload, GlobalBucket bucket) - => SendInternal(method, endpoint, payload, BucketGroup.Global, (int)bucket, 0); - private Task SendInternal(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, payload, BucketGroup.Guild, (int)bucket, guildId); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, GlobalBucket bucket) - => SendInternal(method, endpoint, multipartArgs, BucketGroup.Global, (int)bucket, 0); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, multipartArgs, BucketGroup.Guild, (int)bucket, guildId); + private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket) + => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0); + private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GlobalBucket bucket) + => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GuildBucket bucket, ulong guildId) + => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId); - private async Task SendInternal(string method, string endpoint, object payload, BucketGroup group, int bucketId, ulong guildId) + private async Task SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) { var stopwatch = Stopwatch.StartNew(); string json = null; if (payload != null) json = Serialize(payload); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json), group, bucketId, guildId).ConfigureAwait(false); - int bytes = (int)responseStream.Length; + var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); @@ -118,11 +121,11 @@ namespace Discord.API return responseStream; } - private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, BucketGroup group, int bucketId, ulong guildId) + private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) { var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs), group, bucketId, guildId).ConfigureAwait(false); - int bytes = (int)responseStream.Length; + var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); @@ -448,11 +451,11 @@ namespace Discord.API if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); if (args.Offset < 0) throw new ArgumentOutOfRangeException(nameof(args.Offset)); - int limit = args.Limit ?? int.MaxValue; + int limit = args.Limit.IsSpecified ? args.Limit.Value : int.MaxValue; int offset = args.Offset; List result; - if (args.Limit != null) + if (args.Limit.IsSpecified) result = new List((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); else result = new List(); @@ -488,13 +491,13 @@ namespace Discord.API await Send("DELETE", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false); } - public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args) + public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args) { if (args == null) throw new ArgumentNullException(nameof(args)); if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); - return await Send("PATCH", $"guilds/{guildId}/members/{userId}", args).ConfigureAwait(false); + await Send("PATCH", $"guilds/{guildId}/members/{userId}", args).ConfigureAwait(false); } //Guild Roles diff --git a/src/Discord.Net/API/IOptional.cs b/src/Discord.Net/API/IOptional.cs new file mode 100644 index 000000000..51d4f5271 --- /dev/null +++ b/src/Discord.Net/API/IOptional.cs @@ -0,0 +1,8 @@ +namespace Discord.API +{ + public interface IOptional + { + object Value { get; } + bool IsSpecified { get; } + } +} diff --git a/src/Discord.Net/API/Optional.cs b/src/Discord.Net/API/Optional.cs new file mode 100644 index 000000000..172d0ecab --- /dev/null +++ b/src/Discord.Net/API/Optional.cs @@ -0,0 +1,26 @@ +namespace Discord.API +{ + public struct Optional : IOptional + { + /// Gets the value for this paramter, or default(T) if unspecified. + public T Value { get; } + /// Returns true if this value has been specified. + public bool IsSpecified { get; } + + object IOptional.Value => Value; + + /// Creates a new Parameter with the provided value. + public Optional(T value) + { + Value = value; + IsSpecified = true; + } + + /// Implicitly creates a new Parameter from an existing value. + public static implicit operator Optional(T value) => new Optional(value); + /// Implicitly creates a new Parameter from an existing value. + public static implicit operator T(Optional param) => param.Value; + + public override string ToString() => IsSpecified ? (Value?.ToString() ?? null) : null; + } +} diff --git a/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs b/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs index 570259867..e579a98d8 100644 --- a/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs +++ b/src/Discord.Net/API/Rest/CreateChannelInviteParams.cs @@ -5,12 +5,12 @@ namespace Discord.API.Rest public class CreateChannelInviteParams { [JsonProperty("max_age")] - public int MaxAge { get; set; } = 86400; //24 Hours + public Optional MaxAge { get; set; } [JsonProperty("max_uses")] - public int MaxUses { get; set; } = 0; + public Optional MaxUses { get; set; } [JsonProperty("temporary")] - public bool Temporary { get; set; } = false; + public Optional Temporary { get; set; } [JsonProperty("xkcdpass")] - public bool XkcdPass { get; set; } = false; + public Optional XkcdPass { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net/API/Rest/CreateGuildBanParams.cs index b8bf5b5cc..16e2c9846 100644 --- a/src/Discord.Net/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net/API/Rest/CreateGuildBanParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class CreateGuildBanParams { [JsonProperty("delete-message-days")] - public int PruneDays { get; set; } = 0; + public Optional PruneDays { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs index 2badcf27c..11a1487ec 100644 --- a/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net/API/Rest/CreateGuildChannelParams.cs @@ -8,7 +8,8 @@ namespace Discord.API.Rest public string Name { get; set; } [JsonProperty("type")] public ChannelType Type { get; set; } + [JsonProperty("bitrate")] - public int Bitrate { get; set; } + public Optional Bitrate { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildParams.cs b/src/Discord.Net/API/Rest/CreateGuildParams.cs index d8563adbd..dd6e5b8fd 100644 --- a/src/Discord.Net/API/Rest/CreateGuildParams.cs +++ b/src/Discord.Net/API/Rest/CreateGuildParams.cs @@ -10,7 +10,8 @@ namespace Discord.API.Rest public string Name { get; set; } [JsonProperty("region")] public string Region { get; set; } + [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] - public Stream Icon { get; set; } + public Optional Icon { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateMessageParams.cs b/src/Discord.Net/API/Rest/CreateMessageParams.cs index 6fdc8c2ad..457bbe841 100644 --- a/src/Discord.Net/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net/API/Rest/CreateMessageParams.cs @@ -6,9 +6,10 @@ namespace Discord.API.Rest { [JsonProperty("content")] public string Content { get; set; } = ""; + [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public string Nonce { get; set; } = null; + public Optional Nonce { get; set; } [JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool IsTTS { get; set; } = false; + public Optional IsTTS { get; set; } } } diff --git a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs index ec6e5fe79..c14d1c65f 100644 --- a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs +++ b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs @@ -4,6 +4,7 @@ { public int Limit { get; set; } = DiscordConfig.MaxMessagesPerBatch; public Direction RelativeDirection { get; set; } = Direction.Before; - public ulong? RelativeMessageId { get; set; } = null; + + public Optional RelativeMessageId { get; set; } } } diff --git a/src/Discord.Net/API/Rest/GetGuildMembersParams.cs b/src/Discord.Net/API/Rest/GetGuildMembersParams.cs index 59931e934..bf4be2ee0 100644 --- a/src/Discord.Net/API/Rest/GetGuildMembersParams.cs +++ b/src/Discord.Net/API/Rest/GetGuildMembersParams.cs @@ -2,7 +2,7 @@ { public class GetGuildMembersParams { - public int? Limit { get; set; } = null; - public int Offset { get; set; } = 0; + public Optional Limit { get; set; } + public Optional Offset { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs index d02df347d..f102cccca 100644 --- a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs @@ -5,8 +5,8 @@ namespace Discord.API.Rest public class ModifyChannelPermissionsParams { [JsonProperty("allow")] - public uint Allow { get; set; } + public Optional Allow { get; set; } [JsonProperty("deny")] - public uint Deny { get; set; } + public Optional Deny { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs index f1512536f..64aab3181 100644 --- a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs @@ -7,14 +7,14 @@ namespace Discord.API.Rest public class ModifyCurrentUserParams { [JsonProperty("username")] - public string Username { get; set; } + public Optional Username { get; set; } [JsonProperty("email")] - public string Email { get; set; } + public Optional Email { get; set; } [JsonProperty("password")] - public string Password { get; set; } + public Optional Password { get; set; } [JsonProperty("new_password")] - public string NewPassword { get; set; } + public Optional NewPassword { get; set; } [JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] - public Stream Avatar { get; set; } + public Optional Avatar { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs index b82f7b8d2..374f87377 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildChannelParams.cs @@ -5,8 +5,8 @@ namespace Discord.API.Rest public class ModifyGuildChannelParams { [JsonProperty("name")] - public string Name { get; set; } + public Optional Name { get; set; } [JsonProperty("position")] - public int Position { get; set; } + public Optional Position { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs index 73ec46f7d..8444bb598 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs @@ -5,8 +5,8 @@ namespace Discord.API.Rest public class ModifyGuildChannelsParams { [JsonProperty("id")] - public ulong Id { get; set; } + public Optional Id { get; set; } [JsonProperty("position")] - public int Position { get; set; } + public Optional Position { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs index 731497223..f717b4d52 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs @@ -6,8 +6,8 @@ namespace Discord.API.Rest public class ModifyGuildEmbedParams { [JsonProperty("enabled")] - public bool Enabled { get; set; } - [JsonProperty("channel"), JsonConverter(typeof(UInt64EntityConverter))] - public IVoiceChannel Channel { get; set; } + public Optional Enabled { get; set; } + [JsonProperty("channel")] + public Optional Channel { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs b/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs index 9a5e9f81c..c58971c73 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildIntegrationParams.cs @@ -5,10 +5,10 @@ namespace Discord.API.Rest public class ModifyGuildIntegrationParams { [JsonProperty("expire_behavior")] - public int ExpireBehavior { get; set; } + public Optional ExpireBehavior { get; set; } [JsonProperty("expire_grace_period")] - public int ExpireGracePeriod { get; set; } + public Optional ExpireGracePeriod { get; set; } [JsonProperty("enable_emoticons")] - public bool EnableEmoticons { get; set; } + public Optional EnableEmoticons { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs index 396e0314d..0fbaa6d15 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs @@ -1,17 +1,18 @@ -using Discord.Net.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API.Rest { public class ModifyGuildMemberParams { [JsonProperty("roles")] - public ulong[] Roles { get; set; } + public Optional Roles { get; set; } [JsonProperty("mute")] - public bool Mute { get; set; } + public Optional Mute { get; set; } [JsonProperty("deaf")] - public bool Deaf { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(UInt64EntityConverter))] - public IVoiceChannel VoiceChannel { get; set; } + public Optional Deaf { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("channel_id")] + public Optional VoiceChannel { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildParams.cs b/src/Discord.Net/API/Rest/ModifyGuildParams.cs index 1d5969ab2..e92b1f63c 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildParams.cs @@ -7,20 +7,20 @@ namespace Discord.API.Rest public class ModifyGuildParams { [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("region"), JsonConverterAttribute(typeof(StringEntityConverter))] - public IVoiceRegion Region { get; set; } + public Optional Name { get; set; } + [JsonProperty("region")] + public Optional Region { get; set; } [JsonProperty("verification_level")] - public int VerificationLevel { get; set; } + public Optional VerificationLevel { get; set; } [JsonProperty("afk_channel_id")] - public ulong? AFKChannelId { get; set; } + public Optional AFKChannelId { get; set; } [JsonProperty("afk_timeout")] - public int AFKTimeout { get; set; } + public Optional AFKTimeout { get; set; } [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] - public Stream Icon { get; set; } + public Optional Icon { get; set; } [JsonProperty("owner_id")] - public GuildMember Owner { get; set; } + public Optional Owner { get; set; } [JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] - public Stream Splash { get; set; } + public Optional Splash { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs index 171d9cabe..58a715ae9 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs @@ -5,14 +5,14 @@ namespace Discord.API.Rest public class ModifyGuildRoleParams { [JsonProperty("name")] - public string Name { get; set; } + public Optional Name { get; set; } [JsonProperty("permissions")] - public uint Permissions { get; set; } + public Optional Permissions { get; set; } [JsonProperty("position")] - public int Position { get; set; } + public Optional Position { get; set; } [JsonProperty("color")] - public uint Color { get; set; } + public Optional Color { get; set; } [JsonProperty("hoist")] - public bool Hoist { get; set; } + public Optional Hoist { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs index 7002079d5..286c2463d 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class ModifyGuildRolesParams : ModifyGuildRoleParams { [JsonProperty("id")] - public ulong Id { get; set; } + public Optional Id { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyMessageParams.cs b/src/Discord.Net/API/Rest/ModifyMessageParams.cs index d3b90b903..140bd93e3 100644 --- a/src/Discord.Net/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net/API/Rest/ModifyMessageParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class ModifyMessageParams { [JsonProperty("content")] - public string Content { get; set; } = ""; + public Optional Content { get; set; } = ""; } } diff --git a/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs b/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs index cee07e7f1..28cfb3ee5 100644 --- a/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs +++ b/src/Discord.Net/API/Rest/ModifyTextChannelParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class ModifyTextChannelParams : ModifyGuildChannelParams { [JsonProperty("topic")] - public string Topic { get; set; } + public Optional Topic { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs b/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs index 1fbae95ac..b268783c8 100644 --- a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class ModifyVoiceChannelParams : ModifyGuildChannelParams { [JsonProperty("bitrate")] - public int Bitrate { get; set; } + public Optional Bitrate { get; set; } } } diff --git a/src/Discord.Net/API/Rest/UploadFileParams.cs b/src/Discord.Net/API/Rest/UploadFileParams.cs index 2e1248633..ad3c7bf3f 100644 --- a/src/Discord.Net/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net/API/Rest/UploadFileParams.cs @@ -4,21 +4,22 @@ namespace Discord.API.Rest { public class UploadFileParams { - public string Content { get; set; } = ""; - public string Nonce { get; set; } = null; - public bool IsTTS { get; set; } = false; public string Filename { get; set; } = "unknown.dat"; + public Optional Content { get; set; } + public Optional Nonce { get; set; } + public Optional IsTTS { get; set; } + public IReadOnlyDictionary ToDictionary() { - var dic = new Dictionary - { - ["content"] = Content, - ["tts"] = IsTTS.ToString() - }; - if (Nonce != null) - dic.Add("nonce", Nonce); - return dic; + var d = new Dictionary(); + if (Content.IsSpecified) + d["content"] = Content.Value; + if (IsTTS.IsSpecified) + d["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + d["nonce"] = Nonce.Value; + return d; } } } diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs index 16db2c0e3..48b1a2dcd 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs @@ -90,7 +90,9 @@ namespace Discord /// Gets a collection of all users in this guild. Task> GetUsers(); /// Gets the user in this guild with the provided id, or null if not found. - Task GetUser(ulong id); + Task GetUser(ulong id); + /// Gets the current user for this guild. + Task GetCurrentUser(); Task PruneUsers(int days = 30, bool simulate = false); } } \ No newline at end of file diff --git a/src/Discord.Net/Common/Entities/Users/IUser.cs b/src/Discord.Net/Common/Entities/Users/IUser.cs index 550c53e8b..4998d0419 100644 --- a/src/Discord.Net/Common/Entities/Users/IUser.cs +++ b/src/Discord.Net/Common/Entities/Users/IUser.cs @@ -12,6 +12,8 @@ namespace Discord ushort Discriminator { get; } /// Returns true if this user is a bot account. bool IsBot { get; } + /// Returns true is this user is the current logged-in account. + bool IsCurrentUser { get; } /// Gets the current status of this user. UserStatus Status { get; } /// Gets the username for this user. diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index f7c9e7a8c..a681e2931 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -66,6 +66,8 @@ + + @@ -95,6 +97,8 @@ + + diff --git a/src/Discord.Net/Net/Converters/ImageConverter.cs b/src/Discord.Net/Net/Converters/ImageConverter.cs index cf501050e..5fc25e8d2 100644 --- a/src/Discord.Net/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net/Net/Converters/ImageConverter.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Discord.API; +using Newtonsoft.Json; using System; using System.IO; @@ -6,7 +7,7 @@ namespace Discord.Net.Converters { public class ImageConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(Stream); + public override bool CanConvert(Type objectType) => objectType == typeof(Stream) || objectType == typeof(Optional); public override bool CanRead => true; public override bool CanWrite => true; @@ -17,6 +18,8 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + if (value is Optional) + value = (Optional)value; var stream = value as Stream; byte[] bytes = new byte[stream.Length - stream.Position]; diff --git a/src/Discord.Net/Net/Converters/OptionalContractResolver.cs b/src/Discord.Net/Net/Converters/OptionalContractResolver.cs new file mode 100644 index 000000000..cc0705671 --- /dev/null +++ b/src/Discord.Net/Net/Converters/OptionalContractResolver.cs @@ -0,0 +1,34 @@ +using Discord.API; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Discord.Net.Converters +{ + public class OptionalContractResolver : DefaultContractResolver + { + private static readonly PropertyInfo _isSpecified = typeof(IOptional).GetProperty(nameof(IOptional.IsSpecified)); + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + var type = property.PropertyType; + + if (member.MemberType == MemberTypes.Property) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + { + var parentArg = Expression.Parameter(typeof(object)); + var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); + var isSpecified = Expression.Property(optional, _isSpecified); + var lambda = Expression.Lambda>(isSpecified, parentArg).Compile(); + property.ShouldSerialize = x => lambda(x); + } + } + + return property; + } + } +} diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs new file mode 100644 index 000000000..e75769e5f --- /dev/null +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -0,0 +1,23 @@ +using Discord.API; +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + public class OptionalConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>); + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, (value as IOptional).Value); + } + } +} diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 0aec73597..356c67fb7 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -33,6 +32,8 @@ namespace Discord.Net.Rest UseProxy = false, PreAuthenticate = false }); + + SetHeader("accept-encoding", "gzip, deflate"); } protected virtual void Dispose(bool disposing) { @@ -54,18 +55,18 @@ namespace Discord.Net.Rest _client.DefaultRequestHeaders.Add(key, value); } - public async Task Send(string method, string endpoint, string json = null) + public async Task Send(string method, string endpoint, string json = null, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (json != null) restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false); } } - public async Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams) + public async Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) @@ -95,11 +96,11 @@ namespace Discord.Net.Rest } } restRequest.Content = content; - return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false); } } - private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken) + private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { int retryCount = 0; while (true) @@ -125,7 +126,10 @@ namespace Discord.Net.Rest throw new HttpException(response.StatusCode); } - return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + if (headerOnly) + return null; + else + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } } diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index c340c7c79..6ebc360b2 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -8,7 +8,7 @@ namespace Discord.Net.Rest { void SetHeader(string key, string value); - Task Send(string method, string endpoint, string json = null); - Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams); + Task Send(string method, string endpoint, string json = null, bool headerOnly = false); + Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false); } } diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs index 4e8a3c520..2d14bc367 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs @@ -83,9 +83,9 @@ namespace Discord.Net.Rest { Stream stream; if (request.IsMultipart) - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams).ConfigureAwait(false); + stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false); else - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json).ConfigureAwait(false); + stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json, request.HeaderOnly).ConfigureAwait(false); request.Promise.SetResult(stream); } catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs index 86c7ca962..098dccc8a 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs +++ b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs @@ -9,29 +9,31 @@ namespace Discord.Net.Rest public string Method { get; } public string Endpoint { get; } public string Json { get; } + public bool HeaderOnly { get; } public IReadOnlyDictionary MultipartParams { get; } public TaskCompletionSource Promise { get; } public bool IsMultipart => MultipartParams != null; - public RestRequest(string method, string endpoint, string json) - : this(method, endpoint) + public RestRequest(string method, string endpoint, string json, bool headerOnly) + : this(method, endpoint, headerOnly) { Json = json; } - public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams) - : this(method, endpoint) + public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) + : this(method, endpoint, headerOnly) { MultipartParams = multipartParams; } - private RestRequest(string method, string endpoint) + private RestRequest(string method, string endpoint, bool headerOnly) { Method = method; Endpoint = endpoint; Json = null; MultipartParams = null; + HeaderOnly = headerOnly; Promise = new TaskCompletionSource(); } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index 60170d0fa..ce530d0e2 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -306,7 +306,6 @@ namespace Discord.Rest var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false); return models.Select(x => new GuildUser(this, x)); } - /// Gets the user in this guild with the provided id, or null if not found. public async Task GetUser(ulong id) { @@ -315,7 +314,11 @@ namespace Discord.Rest return new GuildUser(this, model); return null; } - + /// Gets a the current user. + public async Task GetCurrentUser() + { + return await GetUser(Discord.CurrentUser.Id).ConfigureAwait(false); + } public async Task PruneUsers(int days = 30, bool simulate = false) { var args = new GuildPruneParams() { Days = days }; @@ -367,6 +370,8 @@ namespace Discord.Rest => Task.FromResult>(Roles); async Task IGuild.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false); + async Task IGuild.GetCurrentUser() + => await GetCurrentUser().ConfigureAwait(false); async Task> IGuild.GetUsers() => await GetUsers().ConfigureAwait(false); } diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index b62656dfc..5ee975b21 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; @@ -82,8 +83,15 @@ namespace Discord.Rest var args = new ModifyGuildMemberParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); - Update(model); + await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + if (args.Deaf.IsSpecified) + IsDeaf = args.Deaf; + if (args.Mute.IsSpecified) + IsMute = args.Mute; + if (args.Nickname.IsSpecified) + Nickname = args.Nickname; + if (args.Roles.IsSpecified) + _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs index 9572c6620..70e199378 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Rest/Entities/Users/User.cs @@ -28,6 +28,8 @@ namespace Discord.Rest public string Mention => MentionHelper.Mention(this, false); /// public string NicknameMention => MentionHelper.Mention(this, true); + /// + public bool IsCurrentUser => Id == Discord.CurrentUser.Id; internal User(Model model) { From 93bf06bcb6653457a427f609c294396ae3cf3318 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 12 May 2016 23:43:34 -0300 Subject: [PATCH 08/13] Added support for changing nicknames --- src/Discord.Net/API/DiscordRawClient.cs | 7 ++++ .../API/Rest/ModifyCurrentUserNickParams.cs | 10 ++++++ src/Discord.Net/Discord.Net.csproj | 1 + .../Rest/Entities/Users/GuildUser.cs | 32 +++++++++++++------ 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index effc5fde2..d641c7947 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -738,6 +738,13 @@ namespace Discord.API return await Send("PATCH", "users/@me", args).ConfigureAwait(false); } + public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (args.Nickname == "") throw new ArgumentOutOfRangeException(nameof(args.Nickname)); + + await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args).ConfigureAwait(false); + } public async Task CreateDMChannel(CreateDMChannelParams args) { if (args == null) throw new ArgumentNullException(nameof(args)); diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs b/src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs new file mode 100644 index 000000000..38cd54991 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyCurrentUserNickParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class ModifyCurrentUserNickParams + { + [JsonProperty("nick")] + public string Nickname { get; set; } + } +} diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index a681e2931..b178e48e5 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -70,6 +70,7 @@ + diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 5ee975b21..79aa60093 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -83,15 +83,29 @@ namespace Discord.Rest var args = new ModifyGuildMemberParams(); func(args); - await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); - if (args.Deaf.IsSpecified) - IsDeaf = args.Deaf; - if (args.Mute.IsSpecified) - IsMute = args.Mute; - if (args.Nickname.IsSpecified) - Nickname = args.Nickname; - if (args.Roles.IsSpecified) - _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); + + bool isCurrentUser = IsCurrentUser; + if (isCurrentUser && args.Nickname.IsSpecified) + { + var nickArgs = new ModifyCurrentUserNickParams + { + Nickname = args.Nickname.Value + }; + await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + } + + if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) + { + await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + if (args.Deaf.IsSpecified) + IsDeaf = args.Deaf; + if (args.Mute.IsSpecified) + IsMute = args.Mute; + if (args.Nickname.IsSpecified) + Nickname = args.Nickname; + if (args.Roles.IsSpecified) + _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); + } } From 9a47de65091d48d5a64286983ee1719e753dab3a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 13 May 2016 01:40:34 -0300 Subject: [PATCH 09/13] Moved DiscordClient.CurrentUser to GetCurrentUser, lazy load it. --- src/Discord.Net/API/DiscordRawClient.cs | 5 +++ .../Common/Entities/Guilds/IGuild.cs | 2 - .../Common/Entities/Messages/IMessage.cs | 2 - .../Common/Entities/Users/IUser.cs | 2 - src/Discord.Net/IDiscordClient.cs | 7 ++- src/Discord.Net/Net/Rest/IRestClient.cs | 1 + .../Net/Rest/RequestQueue/IRequestQueue.cs | 1 + src/Discord.Net/Rest/DiscordClient.cs | 43 ++++++++++++------- .../Rest/Entities/Channels/DMChannel.cs | 25 +++++------ src/Discord.Net/Rest/Entities/Guilds/Guild.cs | 9 +--- src/Discord.Net/Rest/Entities/Message.cs | 2 - .../Rest/Entities/Users/GuildUser.cs | 3 +- src/Discord.Net/Rest/Entities/Users/User.cs | 2 - 13 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index d641c7947..c01696448 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -25,10 +25,15 @@ namespace Discord.API private readonly IRestClient _restClient; private readonly CancellationToken _cancelToken; private readonly JsonSerializer _serializer; + + public TokenType AuthTokenType { get; private set; } + public IRestClient RestClient { get; private set; } + public IRequestQueue RequestQueue { get; private set; } internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken, TokenType authTokenType, string authToken) { _cancelToken = cancelToken; + AuthTokenType = authTokenType; switch (authTokenType) { diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs index 48b1a2dcd..9d6518612 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs @@ -11,8 +11,6 @@ namespace Discord int AFKTimeout { get; } /// Returns true if this guild is embeddable (e.g. widget) bool IsEmbeddable { get; } - /// Returns true if the current user owns this guild. - bool IsOwner { get; } /// Gets the name of this guild. string Name { get; } int VerificationLevel { get; } diff --git a/src/Discord.Net/Common/Entities/Messages/IMessage.cs b/src/Discord.Net/Common/Entities/Messages/IMessage.cs index 54f2df870..1e5eb3b3f 100644 --- a/src/Discord.Net/Common/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Common/Entities/Messages/IMessage.cs @@ -9,8 +9,6 @@ namespace Discord { /// Gets the time of this message's last edit, if any. DateTime? EditedTimestamp { get; } - /// Returns true if this message originated from the logged-in account. - bool IsAuthor { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns the original, unprocessed text for this message. diff --git a/src/Discord.Net/Common/Entities/Users/IUser.cs b/src/Discord.Net/Common/Entities/Users/IUser.cs index 4998d0419..550c53e8b 100644 --- a/src/Discord.Net/Common/Entities/Users/IUser.cs +++ b/src/Discord.Net/Common/Entities/Users/IUser.cs @@ -12,8 +12,6 @@ namespace Discord ushort Discriminator { get; } /// Returns true if this user is a bot account. bool IsBot { get; } - /// Returns true is this user is the current logged-in account. - bool IsCurrentUser { get; } /// Gets the current status of this user. UserStatus Status { get; } /// Gets the username for this user. diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 2d583126e..b78fb02c6 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,15 +1,18 @@ using Discord.API; +using Discord.Net.Rest; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Discord { + //TODO: Add docstrings public interface IDiscordClient { - ISelfUser CurrentUser { get; } + TokenType AuthTokenType { get; } DiscordRawClient BaseClient { get; } - //IMessageQueue MessageQueue { get; } + IRestClient RestClient { get; } + IRequestQueue RequestQueue { get; } Task Login(TokenType tokenType, string token); Task Logout(); diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index 6ebc360b2..3f99a2f7e 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { + //TODO: Add docstrings public interface IRestClient { void SetHeader(string key, string value); diff --git a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs b/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs index 27231a334..67adbf924 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs +++ b/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { + //TODO: Add docstrings public interface IRequestQueue { Task Clear(GlobalBucket type); diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index ac1bc0762..9a6e0cb36 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -1,11 +1,13 @@ using Discord.API.Rest; using Discord.Logging; +using Discord.Net; using Discord.Net.Rest; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; @@ -22,10 +24,14 @@ namespace Discord.Rest private CancellationTokenSource _cancelTokenSource; private bool _isDisposed; private string _userAgent; - + private SelfUser _currentUser; + public bool IsLoggedIn { get; private set; } - internal API.DiscordRawClient BaseClient { get; private set; } - internal SelfUser CurrentUser { get; private set; } + public API.DiscordRawClient BaseClient { get; private set; } + + public TokenType AuthTokenType => BaseClient.AuthTokenType; + public IRestClient RestClient => BaseClient.RestClient; + public IRequestQueue RequestQueue => BaseClient.RequestQueue; public DiscordClient(DiscordConfig config = null) { @@ -64,9 +70,13 @@ namespace Discord.Rest //MessageQueue = new MessageQueue(RestClient, _restLogger); //await MessageQueue.Start(_cancelTokenSource.Token).ConfigureAwait(false); - - var currentUser = await BaseClient.GetCurrentUser().ConfigureAwait(false); - CurrentUser = new SelfUser(this, currentUser); + + try + { + var currentUser = await BaseClient.GetCurrentUser().ConfigureAwait(false); + _currentUser = new SelfUser(this, currentUser); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized && tokenType == TokenType.Bearer) { } //Ignore 401 if Bearer doesnt have identity _cancelTokenSource = cancelTokenSource; IsLoggedIn = true; @@ -173,25 +183,25 @@ namespace Discord.Rest return new PublicUser(this, model); return null; } - public async Task GetUser(string username, ushort discriminator) + public async Task GetUser(string username, ushort discriminator) { var model = await BaseClient.GetUser(username, discriminator).ConfigureAwait(false); if (model != null) return new PublicUser(this, model); return null; } - public async Task GetCurrentUser() + public async Task GetCurrentUser() { - var currentUser = CurrentUser; - if (currentUser == null) + var user = _currentUser; + if (user == null) { var model = await BaseClient.GetCurrentUser().ConfigureAwait(false); - currentUser = new SelfUser(this, model); - CurrentUser = currentUser; + user = new SelfUser(this, model); + _currentUser = user; } - return currentUser; + return user; } - public async Task> QueryUsers(string query, int limit) + public async Task> QueryUsers(string query, int limit) { var models = await BaseClient.QueryUsers(query, limit).ConfigureAwait(false); return models.Select(x => new PublicUser(this, x)); @@ -225,7 +235,6 @@ namespace Discord.Rest public void Dispose() => Dispose(true); API.DiscordRawClient IDiscordClient.BaseClient => BaseClient; - ISelfUser IDiscordClient.CurrentUser => CurrentUser; async Task IDiscordClient.GetChannel(ulong id) => await GetChannel(id).ConfigureAwait(false); @@ -243,6 +252,10 @@ namespace Discord.Rest => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); async Task IDiscordClient.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false); + async Task IDiscordClient.GetUser(string username, ushort discriminator) + => await GetUser(username, discriminator).ConfigureAwait(false); + async Task IDiscordClient.GetCurrentUser() + => await GetCurrentUser().ConfigureAwait(false); async Task> IDiscordClient.QueryUsers(string query, int limit) => await QueryUsers(query, limit).ConfigureAwait(false); async Task> IDiscordClient.GetVoiceRegions() diff --git a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs index 743af93ab..3a76aa702 100644 --- a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs @@ -20,8 +20,6 @@ namespace Discord.Rest /// public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); - /// - public IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); internal DMChannel(DiscordClient discord, Model model) { @@ -39,20 +37,23 @@ namespace Discord.Rest } /// - public IUser GetUser(ulong id) + public async Task GetUser(ulong id) { + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); if (id == Recipient.Id) return Recipient; - else if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; + else if (id == currentUser.Id) + return currentUser; else return null; } - public IEnumerable GetUsers() + /// + public async Task> GetUsers() { - return ImmutableArray.Create(Discord.CurrentUser, Recipient); + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + return ImmutableArray.Create(currentUser, Recipient); } - + /// public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) { @@ -124,10 +125,10 @@ namespace Discord.Rest IDMUser IDMChannel.Recipient => Recipient; - Task> IChannel.GetUsers() - => Task.FromResult(GetUsers()); - Task IChannel.GetUser(ulong id) - => Task.FromResult(GetUser(id)); + async Task> IChannel.GetUsers() + => await GetUsers().ConfigureAwait(false); + async Task IChannel.GetUser(ulong id) + => await GetUser(id).ConfigureAwait(false); Task IMessageChannel.GetMessage(ulong id) => throw new NotSupportedException(); async Task> IMessageChannel.GetMessages(int limit) diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index ce530d0e2..5763193bc 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -46,8 +46,6 @@ namespace Discord.Rest /// public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); /// - public bool IsOwner => OwnerId == Discord.CurrentUser.Id; - /// public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); /// public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); @@ -157,15 +155,11 @@ namespace Discord.Rest /// public async Task Leave() { - if (IsOwner) - throw new InvalidOperationException("Unable to leave a guild the current user owns."); await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - if (!IsOwner) - throw new InvalidOperationException("Unable to delete a guild the current user does not own."); await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); } @@ -317,7 +311,8 @@ namespace Discord.Rest /// Gets a the current user. public async Task GetCurrentUser() { - return await GetUser(Discord.CurrentUser.Id).ConfigureAwait(false); + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + return await GetUser(currentUser.Id).ConfigureAwait(false); } public async Task PruneUsers(int days = 30, bool simulate = false) { diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index 97ed04512..24c3eb4df 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -41,8 +41,6 @@ namespace Discord.Rest /// public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); - /// - public bool IsAuthor => Discord.CurrentUser.Id == Author.Id; internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; internal Message(IMessageChannel channel, Model model) diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 79aa60093..2e2b11c5c 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -84,7 +84,7 @@ namespace Discord.Rest var args = new ModifyGuildMemberParams(); func(args); - bool isCurrentUser = IsCurrentUser; + bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; if (isCurrentUser && args.Nickname.IsSpecified) { var nickArgs = new ModifyCurrentUserNickParams @@ -92,6 +92,7 @@ namespace Discord.Rest Nickname = args.Nickname.Value }; await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + args.Nickname = new API.Optional(); //Remove } if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs index 70e199378..9572c6620 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Rest/Entities/Users/User.cs @@ -28,8 +28,6 @@ namespace Discord.Rest public string Mention => MentionHelper.Mention(this, false); /// public string NicknameMention => MentionHelper.Mention(this, true); - /// - public bool IsCurrentUser => Id == Discord.CurrentUser.Id; internal User(Model model) { From 96efc97fc1cae1db1cdf242440b260142520ba9a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 13 May 2016 02:54:58 -0300 Subject: [PATCH 10/13] Added user login, switching users/tokens, and token validation --- src/Discord.Net/API/DiscordRawClient.cs | 57 ++++++++++------ src/Discord.Net/API/Rest/LoginParams.cs | 12 ++++ src/Discord.Net/API/Rest/LoginResponse.cs | 10 +++ src/Discord.Net/Discord.Net.csproj | 2 + src/Discord.Net/IDiscordClient.cs | 3 +- src/Discord.Net/Rest/DiscordClient.cs | 66 +++++++++++++------ .../Rest/Entities/Users/GuildUser.cs | 5 +- 7 files changed, 110 insertions(+), 45 deletions(-) create mode 100644 src/Discord.Net/API/Rest/LoginParams.cs create mode 100644 src/Discord.Net/API/Rest/LoginResponse.cs diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index c01696448..31e4701a9 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -29,29 +29,13 @@ namespace Discord.API public TokenType AuthTokenType { get; private set; } public IRestClient RestClient { get; private set; } public IRequestQueue RequestQueue { get; private set; } - - internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken, TokenType authTokenType, string authToken) + + internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken) { _cancelToken = cancelToken; - AuthTokenType = authTokenType; - - switch (authTokenType) - { - case TokenType.Bot: - authToken = $"Bot {authToken}"; - break; - case TokenType.Bearer: - authToken = $"Bearer {authToken}"; - break; - case TokenType.User: - break; - default: - throw new ArgumentException("Unknown oauth token type", nameof(authTokenType)); - } _restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken); _restClient.SetHeader("accept", "*/*"); - _restClient.SetHeader("authorization", authToken); _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); _requestQueue = new RequestQueue(_restClient); @@ -69,6 +53,27 @@ namespace Discord.API _serializer.ContractResolver = new OptionalContractResolver(); } + public void SetToken(TokenType tokenType, string token) + { + AuthTokenType = tokenType; + + switch (tokenType) + { + case TokenType.Bot: + token = $"Bot {token}"; + break; + case TokenType.Bearer: + token = $"Bearer {token}"; + break; + case TokenType.User: + break; + default: + throw new ArgumentException("Unknown oauth token type", nameof(tokenType)); + } + + _restClient.SetHeader("authorization", token); + } + //Core public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) => SendInternal(method, endpoint, null, true, bucket); @@ -122,7 +127,7 @@ namespace Discord.API stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); return responseStream; } @@ -134,11 +139,23 @@ namespace Discord.API stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); return responseStream; } + + //Auth + public async Task Login(LoginParams args) + { + var response = await Send("POST", "auth/login", args).ConfigureAwait(false); + SetToken(TokenType.User, response.Token); + } + public async Task ValidateToken() + { + await Send("GET", "auth/login").ConfigureAwait(false); + } + //Gateway public async Task GetGateway() { diff --git a/src/Discord.Net/API/Rest/LoginParams.cs b/src/Discord.Net/API/Rest/LoginParams.cs new file mode 100644 index 000000000..c5d028063 --- /dev/null +++ b/src/Discord.Net/API/Rest/LoginParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class LoginParams + { + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("password")] + public string Password { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/LoginResponse.cs b/src/Discord.Net/API/Rest/LoginResponse.cs new file mode 100644 index 000000000..2d566612d --- /dev/null +++ b/src/Discord.Net/API/Rest/LoginResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class LoginResponse + { + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index b178e48e5..48f2a4928 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -70,6 +70,8 @@ + + diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index b78fb02c6..4d12632cd 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -14,7 +14,8 @@ namespace Discord IRestClient RestClient { get; } IRequestQueue RequestQueue { get; } - Task Login(TokenType tokenType, string token); + Task Login(string email, string password); + Task Login(TokenType tokenType, string token, bool validateToken = true); Task Logout(); Task GetChannel(ulong id); diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 9a6e0cb36..9ad3fe10a 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -47,43 +47,64 @@ namespace Discord.Rest _log.Message += (s,e) => Log.Raise(this, e); } - public async Task Login(TokenType tokenType, string token) + public async Task Login(string email, string password) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token).ConfigureAwait(false); + await LoginInternal(email, password).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(TokenType tokenType, string token) + public async Task Login(TokenType tokenType, string token, bool validateToken = true) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternal(string email, string password) { if (IsLoggedIn) LogoutInternal(); - try { var cancelTokenSource = new CancellationTokenSource(); - - BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token, tokenType, token); - BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token); - //MessageQueue = new MessageQueue(RestClient, _restLogger); - //await MessageQueue.Start(_cancelTokenSource.Token).ConfigureAwait(false); + var args = new LoginParams { Email = email, Password = password }; + await BaseClient.Login(args).ConfigureAwait(false); + await CompleteLogin(cancelTokenSource, false).ConfigureAwait(false); + } + catch { LogoutInternal(); throw; } + } + private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) + { + if (IsLoggedIn) + LogoutInternal(); + try + { + var cancelTokenSource = new CancellationTokenSource(); + BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token); - try - { - var currentUser = await BaseClient.GetCurrentUser().ConfigureAwait(false); - _currentUser = new SelfUser(this, currentUser); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized && tokenType == TokenType.Bearer) { } //Ignore 401 if Bearer doesnt have identity - - _cancelTokenSource = cancelTokenSource; - IsLoggedIn = true; - LoggedIn.Raise(this); + BaseClient.SetToken(tokenType, token); + await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false); } catch { LogoutInternal(); throw; } } + private async Task CompleteLogin(CancellationTokenSource cancelTokenSource, bool validateToken) + { + BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + + if (validateToken) + await BaseClient.ValidateToken().ConfigureAwait(false); + + _cancelTokenSource = cancelTokenSource; + IsLoggedIn = true; + LoggedIn.Raise(this); + } public async Task Logout() { @@ -99,9 +120,14 @@ namespace Discord.Rest { bool wasLoggedIn = IsLoggedIn; - try { _cancelTokenSource.Cancel(false); } catch { } + if (_cancelTokenSource != null) + { + try { _cancelTokenSource.Cancel(false); } + catch { } + } BaseClient = null; + _currentUser = null; if (wasLoggedIn) { diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 2e2b11c5c..c27c06892 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -87,10 +87,7 @@ namespace Discord.Rest bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; if (isCurrentUser && args.Nickname.IsSpecified) { - var nickArgs = new ModifyCurrentUserNickParams - { - Nickname = args.Nickname.Value - }; + var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value }; await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); args.Nickname = new API.Optional(); //Remove } From 7f3b886479c2da721b928854084150636385015d Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 13 May 2016 03:32:41 -0300 Subject: [PATCH 11/13] Cleaned up Optional --- src/Discord.Net/API/DiscordRawClient.cs | 2 +- src/Discord.Net/API/Optional.cs | 40 +++++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index 31e4701a9..b4b3d1377 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -473,7 +473,7 @@ namespace Discord.API if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); if (args.Offset < 0) throw new ArgumentOutOfRangeException(nameof(args.Offset)); - int limit = args.Limit.IsSpecified ? args.Limit.Value : int.MaxValue; + int limit = args.Limit.GetValueOrDefault(int.MaxValue); int offset = args.Offset; List result; diff --git a/src/Discord.Net/API/Optional.cs b/src/Discord.Net/API/Optional.cs index 172d0ecab..e76d170e5 100644 --- a/src/Discord.Net/API/Optional.cs +++ b/src/Discord.Net/API/Optional.cs @@ -1,26 +1,48 @@ -namespace Discord.API +using System; + +namespace Discord.API { + //Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs public struct Optional : IOptional { + private readonly T _value; + /// Gets the value for this paramter, or default(T) if unspecified. - public T Value { get; } + public T Value + { + get + { + if (!IsSpecified) + throw new InvalidOperationException("This property has no value set."); + return _value; + } + } /// Returns true if this value has been specified. public bool IsSpecified { get; } - object IOptional.Value => Value; + object IOptional.Value => _value; /// Creates a new Parameter with the provided value. public Optional(T value) { - Value = value; + _value = value; IsSpecified = true; } + + public T GetValueOrDefault() => _value; + public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : default(T); - /// Implicitly creates a new Parameter from an existing value. - public static implicit operator Optional(T value) => new Optional(value); - /// Implicitly creates a new Parameter from an existing value. - public static implicit operator T(Optional param) => param.Value; + public override bool Equals(object other) + { + if (!IsSpecified) return other == null; + if (other == null) return false; + return _value.Equals(other); + } - public override string ToString() => IsSpecified ? (Value?.ToString() ?? null) : null; + public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0; + public override string ToString() => IsSpecified ? _value.ToString() : ""; + + public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator T(Optional value) => value.Value; } } From cf6d3d25ec6f3628d0b62507f8b40896a472b0d4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 14 May 2016 18:58:29 -0300 Subject: [PATCH 12/13] Several token and endpoint bugfixes --- src/Discord.Net/API/DiscordRawClient.cs | 27 ++++++++++--------- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 3 ++- src/Discord.Net/Rest/DiscordClient.cs | 7 +++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index b4b3d1377..77d60f907 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -57,18 +57,21 @@ namespace Discord.API { AuthTokenType = tokenType; - switch (tokenType) + if (token != null) { - case TokenType.Bot: - token = $"Bot {token}"; - break; - case TokenType.Bearer: - token = $"Bearer {token}"; - break; - case TokenType.User: - break; - default: - throw new ArgumentException("Unknown oauth token type", nameof(tokenType)); + switch (tokenType) + { + case TokenType.Bot: + token = $"Bot {token}"; + break; + case TokenType.Bearer: + token = $"Bearer {token}"; + break; + case TokenType.User: + break; + default: + throw new ArgumentException("Unknown oauth token type", nameof(tokenType)); + } } _restClient.SetHeader("authorization", token); @@ -485,7 +488,7 @@ namespace Discord.API while (true) { int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; - string endpoint = $"guild/{guildId}/members?limit={limit}&offset={offset}"; + string endpoint = $"guild/{guildId}/members?limit={runLimit}&offset={offset}"; var models = await Send("GET", endpoint).ConfigureAwait(false); //Was this an empty batch? diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 356c67fb7..a2b859197 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -52,7 +52,8 @@ namespace Discord.Net.Rest public void SetHeader(string key, string value) { _client.DefaultRequestHeaders.Remove(key); - _client.DefaultRequestHeaders.Add(key, value); + if (value != null) + _client.DefaultRequestHeaders.Add(key, value); } public async Task Send(string method, string endpoint, string json = null, bool headerOnly = false) diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 9ad3fe10a..c719fab3e 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -43,6 +43,7 @@ namespace Discord.Rest _connectionLock = new SemaphoreSlim(1, 1); _log = new LogManager(config.LogLevel); _userAgent = DiscordConfig.UserAgent; + BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token); _log.Message += (s,e) => Log.Raise(this, e); } @@ -72,7 +73,6 @@ namespace Discord.Rest try { var cancelTokenSource = new CancellationTokenSource(); - BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token); var args = new LoginParams { Email = email, Password = password }; await BaseClient.Login(args).ConfigureAwait(false); @@ -87,7 +87,6 @@ namespace Discord.Rest try { var cancelTokenSource = new CancellationTokenSource(); - BaseClient = new API.DiscordRawClient(_restClientProvider, cancelTokenSource.Token); BaseClient.SetToken(tokenType, token); await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false); @@ -126,7 +125,7 @@ namespace Discord.Rest catch { } } - BaseClient = null; + BaseClient.SetToken(TokenType.User, null); _currentUser = null; if (wasLoggedIn) @@ -186,7 +185,7 @@ namespace Discord.Rest { var model = await BaseClient.GetGuildEmbed(id).ConfigureAwait(false); if (model != null) - return new GuildEmbed(this, model); + return new GuildEmbed(model); return null; } public async Task> GetGuilds() From 1ab4e1e24644cf3d84bf529078ad4611f8c6e6f3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 14 May 2016 18:59:10 -0300 Subject: [PATCH 13/13] Fixed GetGuildMembers endpoint --- src/Discord.Net/API/DiscordRawClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index 77d60f907..60b571f74 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -488,7 +488,7 @@ namespace Discord.API while (true) { int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; - string endpoint = $"guild/{guildId}/members?limit={runLimit}&offset={offset}"; + string endpoint = $"guilds/{guildId}/members?limit={runLimit}&offset={offset}"; var models = await Send("GET", endpoint).ConfigureAwait(false); //Was this an empty batch?