diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordRawClient.cs index 575ece0c8..60b571f74 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordRawClient.cs @@ -21,33 +21,26 @@ namespace Discord.API { internal event EventHandler SentRequest; + private readonly RequestQueue _requestQueue; private readonly IRestClient _restClient; private readonly CancellationToken _cancelToken; private readonly JsonSerializer _serializer; - - internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken, TokenType authTokenType, string authToken) + + public TokenType AuthTokenType { get; private set; } + public IRestClient RestClient { get; private set; } + public IRequestQueue RequestQueue { get; private set; } + + internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken) { _cancelToken = cancelToken; - 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("authorization", authToken); + _restClient.SetHeader("accept", "*/*"); _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()); @@ -57,116 +50,113 @@ namespace Discord.API _serializer.Converters.Add(new UInt64Converter()); _serializer.Converters.Add(new UInt64EntityConverter()); _serializer.Converters.Add(new UserStatusConverter()); + _serializer.ContractResolver = new OptionalContractResolver(); } - //Core - public async Task Send(string method, string endpoint) - where TResponse : class + public void SetToken(TokenType tokenType, string token) { - 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); + AuthTokenType = tokenType; - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); - - 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 (token != null) { - if (!HandleException(ex)) - throw; - return; + 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)); + } } - stopwatch.Stop(); - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); + _restClient.SetHeader("authorization", token); } - public async Task Send(string method, string endpoint, object payload) + + //Core + public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) + => SendInternal(method, endpoint, null, true, bucket); + public Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) + => 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, true, bucket); + public async Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) + where TResponse : class + => 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, 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, false, bucket).ConfigureAwait(false)); + + public Task Send(string method, string endpoint, GuildBucket bucket, ulong 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, true, bucket, guildId); + public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong 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, 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, 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, false, bucket, guildId).ConfigureAwait(false)); + + 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, bool headerOnly, 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; - } - int bytes = (int)responseStream.Length; + string json = null; + if (payload != null) + json = Serialize(payload); + 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(); - var response = Deserialize(responseStream); double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + SentRequest?.Invoke(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, bool headerOnly, 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; - } + 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); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); + SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + + return responseStream; } - 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); - stopwatch.Stop(); - var response = Deserialize(responseStream); - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, (int)responseStream.Length, milliseconds)); - return response; + //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 Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs) + public async Task ValidateToken() { - var stopwatch = Stopwatch.StartNew(); - await _restClient.Send(method, endpoint).ConfigureAwait(false); - stopwatch.Stop(); - - double milliseconds = ToMilliseconds(stopwatch); - SentRequest(this, new SentRequestEventArgs(method, endpoint, 0, milliseconds)); + await Send("GET", "auth/login").ConfigureAwait(false); } //Gateway @@ -257,7 +247,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; @@ -486,11 +476,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.GetValueOrDefault(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(); @@ -498,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 = $"guilds/{guildId}/members?limit={runLimit}&offset={offset}"; var models = await Send("GET", endpoint).ConfigureAwait(false); //Was this an empty batch? @@ -514,8 +504,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) { @@ -524,13 +516,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 @@ -573,7 +565,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: @@ -618,34 +610,57 @@ 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 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)); @@ -653,22 +668,31 @@ namespace Discord.API switch (messageIds.Length) { case 0: - throw new ArgumentOutOfRangeException(nameof(args.MessageIds)); + return; 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) { @@ -739,6 +763,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)); @@ -775,11 +806,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/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..e76d170e5 --- /dev/null +++ b/src/Discord.Net/API/Optional.cs @@ -0,0 +1,48 @@ +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 + { + 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; + + /// Creates a new Parameter with the provided value. + public Optional(T value) + { + _value = value; + IsSpecified = true; + } + + public T GetValueOrDefault() => _value; + public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : default(T); + + public override bool Equals(object other) + { + if (!IsSpecified) return other == null; + if (other == null) return false; + return _value.Equals(other); + } + + 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; + } +} 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/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/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/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/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 2c3d8a3c8..9d6518612 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Common/Entities/Guilds/IGuild.cs @@ -88,7 +88,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/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/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj index a178e7f8e..48f2a4928 100644 --- a/src/Discord.Net/Discord.Net.csproj +++ b/src/Discord.Net/Discord.Net.csproj @@ -66,8 +66,13 @@ + + + + + @@ -95,6 +100,15 @@ + + + + + + + + + @@ -181,7 +195,7 @@ - + diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 2d583126e..4d12632cd 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,17 +1,21 @@ 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 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/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/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/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..a2b859197 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; -using System; +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; @@ -32,6 +32,8 @@ namespace Discord.Net.Rest UseProxy = false, PreAuthenticate = false }); + + SetHeader("accept-encoding", "gzip, deflate"); } protected virtual void Dispose(bool disposing) { @@ -50,21 +52,22 @@ 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) + 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)) @@ -94,11 +97,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) @@ -118,9 +121,16 @@ 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); + if (headerOnly) + return null; + else + 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/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index c340c7c79..3f99a2f7e 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -4,11 +4,12 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { + //TODO: Add docstrings public interface IRestClient { 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/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..67adbf924 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + //TODO: Add docstrings + 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..2d14bc367 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs @@ -0,0 +1,225 @@ +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 bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed + private int _id; + + 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); + _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); + 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)) break; + + try + { + Stream stream; + if (request.IsMultipart) + 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, request.HeaderOnly).ConfigureAwait(false); + request.Promise.SetResult(stream); + } + catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own + { + WindowCount = WindowMaxCount; + var task = _resetTask; + if (task != null) + { + 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 + { + Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); + _resetTask = ResetAfter(ex.RetryAfterMilliseconds); + } + 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"); + } + } + + //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 + { + 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); + + 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); + } + 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} {_id}] {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..098dccc8a --- /dev/null +++ b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs @@ -0,0 +1,40 @@ +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 bool HeaderOnly { get; } + public IReadOnlyDictionary MultipartParams { get; } + public TaskCompletionSource Promise { get; } + + public bool IsMultipart => MultipartParams != null; + + public RestRequest(string method, string endpoint, string json, bool headerOnly) + : this(method, endpoint, headerOnly) + { + Json = json; + } + + public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) + : this(method, endpoint, headerOnly) + { + MultipartParams = multipartParams; + } + + 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/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 5b0e0d119..c719fab3e 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) { @@ -37,43 +43,67 @@ 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); } - 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($"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); - - //MessageQueue = new MessageQueue(RestClient, _restLogger); - //await MessageQueue.Start(_cancelTokenSource.Token).ConfigureAwait(false); - - var currentUser = await BaseClient.GetCurrentUser().ConfigureAwait(false); - CurrentUser = new SelfUser(this, currentUser); - - _cancelTokenSource = cancelTokenSource; - IsLoggedIn = true; - LoggedIn.Raise(this); + + 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.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() { @@ -89,9 +119,14 @@ namespace Discord.Rest { bool wasLoggedIn = IsLoggedIn; - try { _cancelTokenSource.Cancel(false); } catch { } + if (_cancelTokenSource != null) + { + try { _cancelTokenSource.Cancel(false); } + catch { } + } - BaseClient = null; + BaseClient.SetToken(TokenType.User, null); + _currentUser = null; if (wasLoggedIn) { @@ -150,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() @@ -173,25 +208,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 +260,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 +277,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/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/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/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index bcc73e993..5763193bc 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) @@ -300,7 +300,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) { @@ -309,7 +308,12 @@ namespace Discord.Rest return new GuildUser(this, model); return null; } - + /// Gets a the current user. + public async Task GetCurrentUser() + { + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + return await GetUser(currentUser.Id).ConfigureAwait(false); + } public async Task PruneUsers(int days = 30, bool simulate = false) { var args = new GuildPruneParams() { Days = days }; @@ -333,6 +337,8 @@ namespace Discord.Rest } } + public override string ToString() => Name ?? Id.ToString(); + IEnumerable IGuild.Emojis => Emojis; ulong IGuild.EveryoneRoleId => EveryoneRole.Id; IEnumerable IGuild.Features => Features; @@ -359,6 +365,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/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 similarity index 87% rename from src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs rename to src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs index 7e4f41fbd..cae71f5ae 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGUild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs @@ -41,15 +41,17 @@ 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); } + + 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 43abbebc7..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) @@ -68,7 +66,7 @@ namespace Discord.Rest Attachments = ImmutableArray.Create(attachments); } else - Attachments = ImmutableArray.Empty; + Attachments = Array.Empty(); if (model.Embeds.Length > 0) { @@ -78,18 +76,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) @@ -121,7 +119,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); } @@ -131,6 +135,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/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index 1df857695..c27c06892 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; @@ -39,9 +40,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(); } @@ -82,8 +83,27 @@ namespace Discord.Rest var args = new ModifyGuildMemberParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); - Update(model); + + bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; + if (isCurrentUser && args.Nickname.IsSpecified) + { + var nickArgs = new ModifyCurrentUserNickParams { 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) + { + 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 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; ///