From de7e59e1b7a9d06464372255f501b008772a8bae Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 27 Jul 2016 23:35:59 -0400 Subject: [PATCH 01/24] Ignore CHANNEL_PINS_UPDATE This gateway event is raised when a message in a GuildChannel is pinned or unpinned. Unfortunately, this event only contains a timestamp of when the pin was changed, making it effectively useless. As a result, it will be dropped. Resolves #135 --- src/Discord.Net/DiscordSocketClient.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index c85807aba..ddad2a613 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -799,6 +799,11 @@ namespace Discord } } break; + case "CHANNEL_PINS_UPDATE": + { + await _gatewayLogger.DebugAsync("Ignored Disbatch (CHANNEL_PINS_UPDATE)"); + } + break; case "CHANNEL_DELETE": { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); From 366c3bb50ecd04a349ebdc66f5e554ae96e5c7d9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 03:34:05 -0300 Subject: [PATCH 02/24] Use users/@me for token validation --- src/Discord.Net/API/DiscordAPIClient.cs | 12 +++++------- src/Discord.Net/DiscordRestClient.cs | 14 +++++++++++--- src/Discord.Net/Entities/Application.cs | 2 +- src/Discord.Net/Entities/Users/SelfUser.cs | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 81ca94d89..870950142 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -351,12 +351,6 @@ namespace Discord.API await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - //Application - public async Task GetMyApplicationInfoAsync(RequestOptions options = null) - { - return await SendAsync("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false); - } - //Auth public async Task ValidateTokenAsync(RequestOptions options = null) { @@ -1176,7 +1170,7 @@ namespace Discord.API } //Current User/DMs - public async Task GetSelfAsync(RequestOptions options = null) + public async Task GetMyUserAsync(RequestOptions options = null) { return await SendAsync("GET", "users/@me", options: options).ConfigureAwait(false); } @@ -1192,6 +1186,10 @@ namespace Discord.API { return await SendAsync>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); } + public async Task GetMyApplicationAsync(RequestOptions options = null) + { + return await SendAsync("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false); + } public async Task ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); diff --git a/src/Discord.Net/DiscordRestClient.cs b/src/Discord.Net/DiscordRestClient.cs index 253c624f1..829753b64 100644 --- a/src/Discord.Net/DiscordRestClient.cs +++ b/src/Discord.Net/DiscordRestClient.cs @@ -94,7 +94,14 @@ namespace Discord { try { - await ApiClient.ValidateTokenAsync().ConfigureAwait(false); + var user = await GetCurrentUserAsync().ConfigureAwait(false); + if (user == null) //Is using a cached DiscordClient + user = new SelfUser(this, await ApiClient.GetMyUserAsync().ConfigureAwait(false)); + + if (user.IsBot && tokenType == TokenType.User) + throw new InvalidOperationException($"A bot token used provided with {nameof(TokenType)}.{nameof(TokenType.User)}"); + else if (!user.IsBot && tokenType == TokenType.Bot) //Discord currently sends a 401 in this case + throw new InvalidOperationException($"A user token used provided with {nameof(TokenType)}.{nameof(TokenType.Bot)}"); } catch (HttpException ex) { @@ -146,7 +153,7 @@ namespace Discord /// public async Task GetApplicationInfoAsync() { - var model = await ApiClient.GetMyApplicationInfoAsync().ConfigureAwait(false); + var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false); return new Application(this, model); } @@ -266,12 +273,13 @@ namespace Discord var user = _currentUser; if (user == null) { - var model = await ApiClient.GetSelfAsync().ConfigureAwait(false); + var model = await ApiClient.GetMyUserAsync().ConfigureAwait(false); user = new SelfUser(this, model); _currentUser = user; } return user; } + /// public virtual async Task> QueryUsersAsync(string query, int limit) { diff --git a/src/Discord.Net/Entities/Application.cs b/src/Discord.Net/Entities/Application.cs index 677dd2aab..8b3e64b74 100644 --- a/src/Discord.Net/Entities/Application.cs +++ b/src/Discord.Net/Entities/Application.cs @@ -42,7 +42,7 @@ namespace Discord { if (IsAttached) throw new NotSupportedException(); - var response = await Discord.ApiClient.GetMyApplicationInfoAsync().ConfigureAwait(false); + var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); if (response.Id != Id) throw new InvalidOperationException("Unable to update this object from a different application token."); Update(response, UpdateSource.Rest); diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 0169ef6cd..dca9ae837 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -43,7 +43,7 @@ namespace Discord { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetSelfAsync().ConfigureAwait(false); + var model = await Discord.ApiClient.GetMyUserAsync().ConfigureAwait(false); Update(model, UpdateSource.Rest); } public async Task ModifyAsync(Action func) From 4a8b73f3fbf7cf7c570aea856d5c600352723f0e Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 14 Jul 2016 22:11:28 -0300 Subject: [PATCH 03/24] Initial RPC commit, Added Authorize and Authenticate --- src/Discord.Net/API/DiscordAPIClient.cs | 36 +- src/Discord.Net/API/DiscordRpcAPIClient.cs | 288 ++++++++++++++++ src/Discord.Net/API/Rpc/Application.cs | 18 + src/Discord.Net/API/Rpc/AuthenticateEvent.cs | 17 + src/Discord.Net/API/Rpc/AuthenticateParams.cs | 10 + src/Discord.Net/API/Rpc/AuthorizeEvent.cs | 11 + src/Discord.Net/API/Rpc/AuthorizeParams.cs | 12 + src/Discord.Net/API/Rpc/ErrorEvent.cs | 12 + src/Discord.Net/API/Rpc/GuildStatusEvent.cs | 12 + src/Discord.Net/API/Rpc/ReadyEvent.cs | 12 + src/Discord.Net/API/Rpc/RpcConfig.cs | 14 + src/Discord.Net/API/Rpc/RpcMessage.cs | 18 + src/Discord.Net/DiscordRestClient.cs | 13 +- src/Discord.Net/DiscordRpcClient.cs | 379 +++++++++++++++++++++ src/Discord.Net/DiscordRpcConfig.cs | 23 ++ src/Discord.Net/DiscordSocketConfig.cs | 5 - src/Discord.Net/Entities/Channels/GroupChannel.cs | 2 +- .../Entities/WebSocket/Channels/MessageManager.cs | 2 +- .../Net/Queue/Definitions/GlobalBucket.cs | 5 +- src/Discord.Net/Net/Queue/RequestQueue.cs | 5 +- 20 files changed, 862 insertions(+), 32 deletions(-) create mode 100644 src/Discord.Net/API/DiscordRpcAPIClient.cs create mode 100644 src/Discord.Net/API/Rpc/Application.cs create mode 100644 src/Discord.Net/API/Rpc/AuthenticateEvent.cs create mode 100644 src/Discord.Net/API/Rpc/AuthenticateParams.cs create mode 100644 src/Discord.Net/API/Rpc/AuthorizeEvent.cs create mode 100644 src/Discord.Net/API/Rpc/AuthorizeParams.cs create mode 100644 src/Discord.Net/API/Rpc/ErrorEvent.cs create mode 100644 src/Discord.Net/API/Rpc/GuildStatusEvent.cs create mode 100644 src/Discord.Net/API/Rpc/ReadyEvent.cs create mode 100644 src/Discord.Net/API/Rpc/RpcConfig.cs create mode 100644 src/Discord.Net/API/Rpc/RpcMessage.cs create mode 100644 src/Discord.Net/DiscordRpcClient.cs create mode 100644 src/Discord.Net/DiscordRpcConfig.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 870950142..bcb7691c4 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -106,7 +106,7 @@ namespace Discord.API } } public void Dispose() => Dispose(true); - + public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -121,7 +121,7 @@ namespace Discord.API if (LoginState != LoginState.LoggedOut) await LogoutInternalAsync().ConfigureAwait(false); LoginState = LoginState.LoggingIn; - + try { _loginCancelToken = new CancellationTokenSource(); @@ -172,7 +172,7 @@ namespace Discord.API //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. if (LoginState == LoginState.LoggedOut) return; LoginState = LoginState.LoggingOut; - + try { _loginCancelToken?.Cancel(false); } catch { } @@ -250,7 +250,7 @@ namespace Discord.API if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - + try { _connectCancelToken?.Cancel(false); } catch { } @@ -260,29 +260,29 @@ namespace Discord.API } //REST - public Task SendAsync(string method, string endpoint, + public Task SendAsync(string method, string endpoint, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) => SendInternalAsync(method, endpoint, null, true, BucketGroup.Global, (int)bucket, 0, options); - public Task SendAsync(string method, string endpoint, object payload, + public Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Global, (int)bucket, 0, options); public async Task SendAsync(string method, string endpoint, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false)); - public async Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = + public async Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false)); - - public Task SendAsync(string method, string endpoint, + + public Task SendAsync(string method, string endpoint, GuildBucket bucket, ulong guildId, RequestOptions options = null) => SendInternalAsync(method, endpoint, null, true, BucketGroup.Guild, (int)bucket, guildId, options); - public Task SendAsync(string method, string endpoint, object payload, + public Task SendAsync(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Guild, (int)bucket, guildId, options); - public async Task SendAsync(string method, string endpoint, + public async Task SendAsync(string method, string endpoint, GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); - public async Task SendAsync(string method, string endpoint, object payload, + public async Task SendAsync(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); @@ -311,7 +311,7 @@ namespace Discord.API => SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); //Core - private async Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, + private async Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); @@ -326,7 +326,7 @@ namespace Discord.API return responseStream; } - private async Task SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + private async Task SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); @@ -339,7 +339,7 @@ namespace Discord.API return responseStream; } - private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, + private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, BucketGroup group, int bucketId, ulong guildId, RequestOptions options) { //TODO: Add ETF @@ -913,7 +913,7 @@ namespace Discord.API relativeDir = "around"; break; } - + int runs = (limit + DiscordRestConfig.MaxMessagesPerBatch - 1) / DiscordRestConfig.MaxMessagesPerBatch; int lastRunCount = limit - (runs - 1) * DiscordRestConfig.MaxMessagesPerBatch; var result = new API.Message[runs][]; @@ -1027,7 +1027,7 @@ namespace Discord.API { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); - + if (args._content.GetValueOrDefault(null) == null) args._content = ""; else if (args._content.IsSpecified) @@ -1153,7 +1153,7 @@ namespace Discord.API { Preconditions.NotNullOrEmpty(username, nameof(username)); Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); - + try { var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs new file mode 100644 index 000000000..328d424db --- /dev/null +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -0,0 +1,288 @@ +using Discord.API.Gateway; +using Discord.API.Rest; +using Discord.API.Rpc; +using Discord.Net; +using Discord.Net.Converters; +using Discord.Net.Queue; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class DiscordRpcApiClient : IDisposable + { + private object _eventLock = new object(); + + public event Func SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentRpcMessageEvent = new AsyncEvent>(); + + public event Func ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } + private readonly AsyncEvent> _receivedRpcEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly RequestQueue _requestQueue; + private readonly JsonSerializer _serializer; + private readonly IWebSocketClient _webSocketClient; + private readonly SemaphoreSlim _connectionLock; + private readonly string _clientId; + private CancellationTokenSource _loginCancelToken, _connectCancelToken; + private string _authToken; + private bool _isDisposed; + + public LoginState LoginState { get; private set; } + public ConnectionState ConnectionState { get; private set; } + + public DiscordRpcApiClient(string clientId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + { + _connectionLock = new SemaphoreSlim(1, 1); + _clientId = clientId; + + _requestQueue = requestQueue ?? new RequestQueue(); + + if (webSocketProvider != null) + { + _webSocketClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _webSocketClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + { + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); + } + } + }; + _webSocketClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(text); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); + }; + _webSocketClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + } + + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + (_webSocketClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); + + public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) + { + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); + + if (tokenType != TokenType.Bearer) + throw new InvalidOperationException("RPC only supports bearer tokens"); + + LoginState = LoginState.LoggingIn; + try + { + _loginCancelToken = new CancellationTokenSource(); + await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); + + _authToken = token; + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task LogoutAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LogoutInternalAsync() + { + //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + try { _loginCancelToken?.Cancel(false); } + catch { } + + await DisconnectInternalAsync().ConfigureAwait(false); + await _requestQueue.ClearAsync().ConfigureAwait(false); + + await _requestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); + + LoginState = LoginState.LoggedOut; + } + + public async Task ConnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync() + { + /*if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting.");*/ + + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + if (_webSocketClient != null) + _webSocketClient.SetCancelToken(_connectCancelToken.Token); + + bool success = false; + for (int port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++) + { + try + { + string url = $"wss://discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; + await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); + success = true; + break; + } + catch (Exception) + { + } + } + if (!success) + throw new Exception("Unable to connect to the RPC server."); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync() + { + if (_webSocketClient == null) + throw new NotSupportedException("This client is not configured with websocket support."); + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _webSocketClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Core + public Task SendRpcAsync(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, RequestOptions options = null) + => SendRpcAsyncInternal(cmd, payload, BucketGroup.Global, (int)bucket, 0, options); + public Task SendRpcAsync(string cmd, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendRpcAsyncInternal(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, options); + private async Task SendRpcAsyncInternal(string cmd, object payload, + BucketGroup group, int bucketId, ulong guildId, RequestOptions options) + { + //TODO: Add Nonce to pair sent requests with responses + byte[] bytes = null; + payload = new RpcMessage { Cmd = cmd, Args = payload, Nonce = Guid.NewGuid().ToString() }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); + } + + //Rpc + public async Task SendAuthenticateAsync(RequestOptions options = null) + { + var msg = new AuthenticateParams() + { + AccessToken = _authToken + }; + await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); + } + public async Task SendAuthorizeAsync(string[] scopes, RequestOptions options = null) + { + var msg = new AuthorizeParams() + { + ClientId = _clientId, + Scopes = scopes + }; + await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); + } + + //Helpers + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + private string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + private T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + } +} diff --git a/src/Discord.Net/API/Rpc/Application.cs b/src/Discord.Net/API/Rpc/Application.cs new file mode 100644 index 000000000..1a5520e69 --- /dev/null +++ b/src/Discord.Net/API/Rpc/Application.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class Application + { + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("rpc_origins")] + public string RpcOrigins { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/AuthenticateEvent.cs b/src/Discord.Net/API/Rpc/AuthenticateEvent.cs new file mode 100644 index 000000000..ca99ce8ff --- /dev/null +++ b/src/Discord.Net/API/Rpc/AuthenticateEvent.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rpc +{ + public class AuthenticateEvent + { + [JsonProperty("application")] + public Application Application { get; set; } + [JsonProperty("expires")] + public DateTimeOffset Expires { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/AuthenticateParams.cs b/src/Discord.Net/API/Rpc/AuthenticateParams.cs new file mode 100644 index 000000000..f64fff8b9 --- /dev/null +++ b/src/Discord.Net/API/Rpc/AuthenticateParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class AuthenticateParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/AuthorizeEvent.cs b/src/Discord.Net/API/Rpc/AuthorizeEvent.cs new file mode 100644 index 000000000..8416d5f86 --- /dev/null +++ b/src/Discord.Net/API/Rpc/AuthorizeEvent.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rpc +{ + public class AuthorizeEvent + { + [JsonProperty("code")] + public string Code { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/AuthorizeParams.cs b/src/Discord.Net/API/Rpc/AuthorizeParams.cs new file mode 100644 index 000000000..bf8bd162a --- /dev/null +++ b/src/Discord.Net/API/Rpc/AuthorizeParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class AuthorizeParams + { + [JsonProperty("client_id")] + public string ClientId { get; set; } + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/ErrorEvent.cs b/src/Discord.Net/API/Rpc/ErrorEvent.cs new file mode 100644 index 000000000..99eee4f4a --- /dev/null +++ b/src/Discord.Net/API/Rpc/ErrorEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class ErrorEvent + { + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GuildStatusEvent.cs b/src/Discord.Net/API/Rpc/GuildStatusEvent.cs new file mode 100644 index 000000000..5990dace4 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GuildStatusEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GuildStatusEvent + { + [JsonProperty("guild")] + public Guild Guild { get; set; } + [JsonProperty("online")] + public int Online { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/ReadyEvent.cs b/src/Discord.Net/API/Rpc/ReadyEvent.cs new file mode 100644 index 000000000..3bd48c12e --- /dev/null +++ b/src/Discord.Net/API/Rpc/ReadyEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class ReadyEvent + { + [JsonProperty("v")] + public int Version { get; set; } + [JsonProperty("config")] + public RpcConfig Config { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/RpcConfig.cs b/src/Discord.Net/API/Rpc/RpcConfig.cs new file mode 100644 index 000000000..07b703367 --- /dev/null +++ b/src/Discord.Net/API/Rpc/RpcConfig.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class RpcConfig + { + [JsonProperty("cdn_host")] + public string CdnHost { get; set; } + [JsonProperty("api_endpoint")] + public string ApiEndpoint { get; set; } + [JsonProperty("environment")] + public string Environment { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/RpcMessage.cs b/src/Discord.Net/API/Rpc/RpcMessage.cs new file mode 100644 index 000000000..9226e0fa8 --- /dev/null +++ b/src/Discord.Net/API/Rpc/RpcMessage.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class RpcMessage + { + [JsonProperty("cmd")] + public string Cmd { get; set; } + [JsonProperty("nonce")] + public string Nonce { get; set; } + [JsonProperty("evt")] + public string Event { get; set; } + [JsonProperty("data")] + public object Data { get; set; } + [JsonProperty("args")] + public object Args { get; set; } + } +} diff --git a/src/Discord.Net/DiscordRestClient.cs b/src/Discord.Net/DiscordRestClient.cs index 829753b64..676ba9200 100644 --- a/src/Discord.Net/DiscordRestClient.cs +++ b/src/Discord.Net/DiscordRestClient.cs @@ -28,9 +28,9 @@ namespace Discord internal readonly ILogger _clientLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; internal readonly RequestQueue _requestQueue; - internal bool _isDisposed; internal SelfUser _currentUser; private bool _isFirstLogSub; + internal bool _isDisposed; public API.DiscordApiClient ApiClient { get; } internal LogManager LogManager { get; } @@ -303,8 +303,10 @@ namespace Discord internal virtual void Dispose(bool disposing) { if (!_isDisposed) + { + ApiClient.Dispose(); _isDisposed = true; - ApiClient.Dispose(); + } } /// public void Dispose() => Dispose(true); @@ -312,10 +314,11 @@ namespace Discord private async Task WriteInitialLog() { if (this is DiscordSocketClient) - await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordSocketConfig.GatewayEncoding})").ConfigureAwait(false); + await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false); + else if (this is DiscordRpcClient) + await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); else - await _clientLogger.InfoAsync($"DiscordRestClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); - + await _clientLogger.InfoAsync($"DiscordClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion})").ConfigureAwait(false); await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs new file mode 100644 index 000000000..cfaaed6c4 --- /dev/null +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -0,0 +1,379 @@ +using Discord.API.Rpc; +using Discord.Logging; +using Discord.Net.Converters; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + public class DiscordRpcClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + private readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } + private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); + public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } + private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); + + public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + + private readonly ILogger _clientLogger, _rpcLogger; + private readonly SemaphoreSlim _connectionLock; + private readonly JsonSerializer _serializer; + + private TaskCompletionSource _connectTask; + private CancellationTokenSource _cancelToken; + internal SelfUser _currentUser; + private Task _reconnectTask; + private bool _isFirstLogSub; + private bool _isReconnecting; + private bool _isDisposed; + private string[] _scopes; + + public API.DiscordRpcApiClient ApiClient { get; } + internal LogManager LogManager { get; } + public LoginState LoginState { get; private set; } + public ConnectionState ConnectionState { get; private set; } + + /// Creates a new RPC discord client. + public DiscordRpcClient(string clientId) : this(new DiscordRpcConfig(clientId)) { } + /// Creates a new RPC discord client. + public DiscordRpcClient(DiscordRpcConfig config) + { + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _clientLogger = LogManager.CreateLogger("Client"); + _rpcLogger = LogManager.CreateLogger("RPC"); + _isFirstLogSub = true; + + _connectionLock = new SemaphoreSlim(1, 1); + + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _rpcLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.WebSocketProvider); + ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.ReceivedRpcEvent += ProcessMessageAsync; + ApiClient.Disconnected += async ex => + { + if (ex != null) + { + await _rpcLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); + await StartReconnectAsync(ex).ConfigureAwait(false); + } + else + await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); + }; + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + ApiClient.Dispose(); + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); + + /// + public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + { + if (_isFirstLogSub) + { + _isFirstLogSub = false; + await WriteInitialLog().ConfigureAwait(false); + } + + if (LoginState != LoginState.LoggedOut) + await LogoutInternalAsync().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + + try + { + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternalAsync().ConfigureAwait(false); + throw; + } + + await _loggedInEvent.InvokeAsync().ConfigureAwait(false); + } + + /// + public async Task LogoutAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LogoutInternalAsync() + { + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + await ApiClient.LogoutAsync().ConfigureAwait(false); + + _currentUser = null; + + LoginState = LoginState.LoggedOut; + + await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); + } + + /// + public async Task ConnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + await ConnectInternalAsync(null).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + public async Task ConnectAndAuthorizeAsync(params string[] scopes) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + await ConnectInternalAsync(scopes).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync(string[] scopes) + { + if (scopes == null && LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting or call ConnectAndAuthorizeAsync."); + _scopes = scopes; + + if (_isFirstLogSub) + { + _isFirstLogSub = false; + await WriteInitialLog().ConfigureAwait(false); + } + + var state = ConnectionState; + if (state == ConnectionState.Connecting || state == ConnectionState.Connected) + await DisconnectInternalAsync(null).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connecting; + await _rpcLogger.InfoAsync("Connecting").ConfigureAwait(false); + try + { + _connectTask = new TaskCompletionSource(); + _cancelToken = new CancellationTokenSource(); + await ApiClient.ConnectAsync().ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); + + /*if (_sessionId != null) + await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + else + await ApiClient.SendIdentifyAsync().ConfigureAwait(false);*/ + + await _connectTask.Task.ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false); + } + catch (Exception) + { + await DisconnectInternalAsync(null).ConfigureAwait(false); + throw; + } + } + /// + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + await DisconnectInternalAsync(null).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync(Exception ex) + { + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + await _rpcLogger.InfoAsync("Disconnecting").ConfigureAwait(false); + + await _rpcLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false); + //Signal tasks to complete + try { _cancelToken.Cancel(); } catch { } + + await _rpcLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false); + //Disconnect from server + await ApiClient.DisconnectAsync().ConfigureAwait(false); + + _scopes = null; + ConnectionState = ConnectionState.Disconnected; + await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false); + + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + } + + private async Task StartReconnectAsync(Exception ex) + { + //TODO: Is this thread-safe? + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(ex).ConfigureAwait(false); + if (_reconnectTask != null) return; + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); + } + finally { _connectionLock.Release(); } + } + private async Task ReconnectInternalAsync() + { + try + { + int nextReconnectDelay = 1000; + while (_isReconnecting) + { + try + { + await Task.Delay(nextReconnectDelay).ConfigureAwait(false); + nextReconnectDelay *= 2; + if (nextReconnectDelay > 30000) + nextReconnectDelay = 30000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(_scopes).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + return; + } + catch (Exception ex) + { + await _rpcLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); + } + } + } + finally + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + _reconnectTask = null; + } + finally { _connectionLock.Release(); } + } + } + + private async Task ProcessMessageAsync(string cmd, string evnt, object payload, string nonce) + { + try + { + switch (cmd) + { + case "DISPATCH": + switch (evnt) + { + //Connection + case "READY": + { + await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + if (_scopes != null) + await ApiClient.SendAuthorizeAsync(_scopes).ConfigureAwait(false); //No bearer + else + await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); //Has bearer + } + break; + + //Others + default: + await _rpcLogger.WarningAsync($"Unknown Dispatch ({evnt})").ConfigureAwait(false); + return; + } + break; + case "AUTHORIZE": + { + await _rpcLogger.DebugAsync("Received AUTHORIZE").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + await ApiClient.LoginAsync(TokenType.Bearer, data.Code).ConfigureAwait(false); + await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); + } + break; + case "AUTHENTICATE": + { + await _rpcLogger.DebugAsync("Received AUTHENTICATE").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + break; + + default: + await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); + return; + } + } + catch (Exception ex) + { + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt != null ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + return; + } + } + + private async Task WriteInitialLog() + { + await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordRestConfig.Version} (RPC v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); + await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); + await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); + await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); + } + private static string ToArchString(Architecture arch) + { + switch (arch) + { + case Architecture.X64: return "x64"; + case Architecture.X86: return "x86"; + default: return arch.ToString(); + } + } + } +} diff --git a/src/Discord.Net/DiscordRpcConfig.cs b/src/Discord.Net/DiscordRpcConfig.cs new file mode 100644 index 000000000..b120f8399 --- /dev/null +++ b/src/Discord.Net/DiscordRpcConfig.cs @@ -0,0 +1,23 @@ +using Discord.Net.WebSockets; + +namespace Discord +{ + public class DiscordRpcConfig : DiscordConfig + { + public const int RpcAPIVersion = 1; + + public const int PortRangeStart = 6463; + public const int PortRangeEnd = 6472; + + public DiscordRpcConfig(string clientId) + { + ClientId = clientId; + } + + /// Gets or sets the Discord client/application id used for this RPC connection. + public string ClientId { get; set; } + + /// Gets or sets the provider used to generate new websocket connections. + public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + } +} diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs index 8bdb3020a..cbb63e514 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -14,11 +14,6 @@ namespace Discord /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheSize { get; set; } = 0; - /*/// - /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage. - /// - public bool UsePermissionsCache { get; set; } = false;*/ /// /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. /// Decreasing this may reduce CPU usage while increasing login time and network usage. diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index 929d050cd..4e0a9024d 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -17,7 +17,7 @@ namespace Discord { protected ConcurrentDictionary _users; private string _iconId; - + public override DiscordRestClient Discord { get; } public string Name { get; private set; } diff --git a/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs b/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs index 984e66f92..2d47e3190 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs @@ -12,7 +12,7 @@ namespace Discord private readonly DiscordSocketClient _discord; private readonly ISocketMessageChannel _channel; - public virtual IReadOnlyCollection Messages + public virtual IReadOnlyCollection Messages => ImmutableArray.Create(); public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) diff --git a/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs b/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs index 7d4ebb761..fe95ecb79 100644 --- a/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs +++ b/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs @@ -5,7 +5,10 @@ GeneralRest, DirectMessage, SendEditMessage, + GeneralGateway, - UpdateStatus + UpdateStatus, + + GeneralRpc } } diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index 826bdd5fa..37e5f816c 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -36,7 +36,10 @@ namespace Discord.Net.Queue //Gateway [GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both), - [GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway) + [GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway), + + //Rpc + [GlobalBucket.GeneralRpc] = new Bucket(null, "rpc", 120, 60, BucketTarget.Both) }.ToImmutableDictionary(); _guildLimits = new Dictionary From 0664442bf1896f2d048cab3618ad167cd840b2a8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 12:58:28 -0300 Subject: [PATCH 04/24] Implemented support for RPC responses and errors, fixed several bugs --- src/Discord.Net/API/DiscordAPIClient.cs | 11 +- src/Discord.Net/API/DiscordRpcAPIClient.cs | 224 +++++++++++++++++---- src/Discord.Net/API/Rpc/Application.cs | 2 +- ...uthenticateEvent.cs => AuthenticateResponse.cs} | 2 +- .../{AuthorizeEvent.cs => AuthorizeResponse.cs} | 2 +- .../API/Rpc/ChannelSubscriptionParams.cs | 10 + src/Discord.Net/API/Rpc/GetChannelParams.cs | 10 + src/Discord.Net/API/Rpc/GetChannelsParams.cs | 10 + src/Discord.Net/API/Rpc/GetChannelsResponse.cs | 10 + src/Discord.Net/API/Rpc/GetGuildParams.cs | 10 + src/Discord.Net/API/Rpc/GetGuildsParams.cs | 11 + src/Discord.Net/API/Rpc/GetGuildsResponse.cs | 10 + src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs | 10 + src/Discord.Net/API/Rpc/MessageEvent.cs | 11 + src/Discord.Net/API/Rpc/RpcChannel.cs | 10 + src/Discord.Net/API/Rpc/RpcGuild.cs | 12 ++ src/Discord.Net/API/Rpc/RpcMessage.cs | 7 +- src/Discord.Net/API/Rpc/RpcUserGuild.cs | 12 ++ .../API/Rpc/SelectVoiceChannelParams.cs | 10 + src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs | 10 + src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs | 12 ++ src/Discord.Net/API/Rpc/SpeakingEvent.cs | 11 + src/Discord.Net/API/Rpc/SubscriptionResponse.cs | 10 + src/Discord.Net/API/Rpc/VoiceStateEvent.cs | 11 + src/Discord.Net/DiscordRestClient.cs | 2 +- src/Discord.Net/DiscordRpcClient.cs | 66 +++--- src/Discord.Net/DiscordRpcConfig.cs | 5 +- src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs | 8 + src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs | 29 +++ src/Discord.Net/Net/RpcException.cs | 17 ++ 30 files changed, 482 insertions(+), 83 deletions(-) rename src/Discord.Net/API/Rpc/{AuthenticateEvent.cs => AuthenticateResponse.cs} (91%) rename src/Discord.Net/API/Rpc/{AuthorizeEvent.cs => AuthorizeResponse.cs} (81%) create mode 100644 src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs create mode 100644 src/Discord.Net/API/Rpc/GetChannelParams.cs create mode 100644 src/Discord.Net/API/Rpc/GetChannelsParams.cs create mode 100644 src/Discord.Net/API/Rpc/GetChannelsResponse.cs create mode 100644 src/Discord.Net/API/Rpc/GetGuildParams.cs create mode 100644 src/Discord.Net/API/Rpc/GetGuildsParams.cs create mode 100644 src/Discord.Net/API/Rpc/GetGuildsResponse.cs create mode 100644 src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs create mode 100644 src/Discord.Net/API/Rpc/MessageEvent.cs create mode 100644 src/Discord.Net/API/Rpc/RpcChannel.cs create mode 100644 src/Discord.Net/API/Rpc/RpcGuild.cs create mode 100644 src/Discord.Net/API/Rpc/RpcUserGuild.cs create mode 100644 src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs create mode 100644 src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs create mode 100644 src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs create mode 100644 src/Discord.Net/API/Rpc/SpeakingEvent.cs create mode 100644 src/Discord.Net/API/Rpc/SubscriptionResponse.cs create mode 100644 src/Discord.Net/API/Rpc/VoiceStateEvent.cs create mode 100644 src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs create mode 100644 src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs create mode 100644 src/Discord.Net/Net/RpcException.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index bcb7691c4..ab12fbb9f 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -71,16 +71,21 @@ namespace Discord.API zlib.CopyTo(decompressed); decompressed.Position = 0; using (var reader = new StreamReader(decompressed)) + using (var jsonReader = new JsonTextReader(reader)) { - var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + var msg = _serializer.Deserialize(jsonReader); await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { - var msg = JsonConvert.DeserializeObject(text); - await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } }; _gatewayClient.Closed += async ex => { diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 328d424db..8a813d8e5 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -1,21 +1,16 @@ -using Discord.API.Gateway; -using Discord.API.Rest; -using Discord.API.Rpc; -using Discord.Net; +using Discord.API.Rpc; +using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Queue; -using Discord.Net.Rest; using Discord.Net.WebSockets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; -using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -24,16 +19,46 @@ namespace Discord.API { public class DiscordRpcApiClient : IDisposable { + private abstract class RpcRequest + { + public abstract Task SetResultAsync(JToken data, JsonSerializer serializer); + public abstract Task SetExceptionAsync(JToken data, JsonSerializer serializer); + } + private class RpcRequest : RpcRequest + { + public TaskCompletionSource Promise { get; set; } + + public RpcRequest(RequestOptions options) + { + Promise = new TaskCompletionSource(); + Task.Run(async () => + { + await Task.Delay(options?.Timeout ?? 15000).ConfigureAwait(false); + Promise.TrySetCanceled(); //Doesn't need to be async, we're already in a separate task + }); + } + public override Task SetResultAsync(JToken data, JsonSerializer serializer) + { + return Promise.TrySetResultAsync(data.ToObject(serializer)); + } + public override Task SetExceptionAsync(JToken data, JsonSerializer serializer) + { + var error = data.ToObject(serializer); + return Promise.TrySetExceptionAsync(new RpcException(error.Code, error.Message)); + } + } + private object _eventLock = new object(); public event Func SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } private readonly AsyncEvent> _sentRpcMessageEvent = new AsyncEvent>(); - public event Func ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } - private readonly AsyncEvent> _receivedRpcEvent = new AsyncEvent>(); + public event Func, Optional, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } + private readonly AsyncEvent, Optional, Task>> _receivedRpcEvent = new AsyncEvent, Optional, Task>>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + private readonly ConcurrentDictionary _requests; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; private readonly IWebSocketClient _webSocketClient; @@ -41,22 +66,26 @@ namespace Discord.API private readonly string _clientId; private CancellationTokenSource _loginCancelToken, _connectCancelToken; private string _authToken; + private string _origin; private bool _isDisposed; public LoginState LoginState { get; private set; } public ConnectionState ConnectionState { get; private set; } - public DiscordRpcApiClient(string clientId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + public DiscordRpcApiClient(string clientId, string origin, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) { _connectionLock = new SemaphoreSlim(1, 1); _clientId = clientId; + _origin = origin; _requestQueue = requestQueue ?? new RequestQueue(); + _requests = new ConcurrentDictionary(); if (webSocketProvider != null) { _webSocketClient = webSocketProvider(); - //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + //_webSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _webSocketClient.SetHeader("origin", _origin); _webSocketClient.BinaryMessage += async (data, index, count) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) @@ -66,16 +95,25 @@ namespace Discord.API zlib.CopyTo(decompressed); decompressed.Position = 0; using (var reader = new StreamReader(decompressed)) + using (var jsonReader = new JsonTextReader(reader)) { - var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); + var msg = _serializer.Deserialize(jsonReader); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); + if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) + ProcessMessage(msg); } } }; _webSocketClient.TextMessage += async text => { - var msg = JsonConvert.DeserializeObject(text); - await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); + if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) + ProcessMessage(msg); + } }; _webSocketClient.Closed += async ex => { @@ -99,19 +137,19 @@ namespace Discord.API } } public void Dispose() => Dispose(true); - - public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) + + public async Task LoginAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); + await LoginInternalAsync(tokenType, token, upgrade, options).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) + private async Task LoginInternalAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) { - if (LoginState != LoginState.LoggedOut) + if (!upgrade && LoginState != LoginState.LoggedOut) await LogoutInternalAsync().ConfigureAwait(false); if (tokenType != TokenType.Bearer) @@ -233,39 +271,155 @@ namespace Discord.API } //Core - public Task SendRpcAsync(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, RequestOptions options = null) - => SendRpcAsyncInternal(cmd, payload, BucketGroup.Global, (int)bucket, 0, options); - public Task SendRpcAsync(string cmd, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendRpcAsyncInternal(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, options); - private async Task SendRpcAsyncInternal(string cmd, object payload, - BucketGroup group, int bucketId, ulong guildId, RequestOptions options) + public Task SendRpcAsync(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, + Optional evt = default(Optional), RequestOptions options = null) + where TResponse : class + => SendRpcAsyncInternal(cmd, payload, BucketGroup.Global, (int)bucket, 0, evt, options); + public Task SendRpcAsync(string cmd, object payload, GuildBucket bucket, ulong guildId, + Optional evt = default(Optional), RequestOptions options = null) + where TResponse : class + => SendRpcAsyncInternal(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, evt, options); + private async Task SendRpcAsyncInternal(string cmd, object payload, BucketGroup group, int bucketId, ulong guildId, + Optional evt, RequestOptions options) + where TResponse : class { - //TODO: Add Nonce to pair sent requests with responses byte[] bytes = null; - payload = new RpcMessage { Cmd = cmd, Args = payload, Nonce = Guid.NewGuid().ToString() }; + var guid = Guid.NewGuid(); + payload = new RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; if (payload != null) - bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + { + var json = SerializeJson(payload); + bytes = Encoding.UTF8.GetBytes(json); + } + + var requestTracker = new RpcRequest(options); + _requests[guid] = requestTracker; + await _requestQueue.SendAsync(new WebSocketRequest(_webSocketClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); await _sentRpcMessageEvent.InvokeAsync(cmd).ConfigureAwait(false); + return await requestTracker.Promise.Task.ConfigureAwait(false); } //Rpc - public async Task SendAuthenticateAsync(RequestOptions options = null) + public async Task SendAuthenticateAsync(RequestOptions options = null) { var msg = new AuthenticateParams() { AccessToken = _authToken }; - await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); + return await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); } - public async Task SendAuthorizeAsync(string[] scopes, RequestOptions options = null) + public async Task SendAuthorizeAsync(string[] scopes, RequestOptions options = null) { var msg = new AuthorizeParams() { ClientId = _clientId, Scopes = scopes }; - await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); + if (options == null) + options = new RequestOptions(); + if (options.Timeout == null) + options.Timeout = 60000; //This requires manual input on the user's end, lets give them more time + return await SendRpcAsync("AUTHORIZE", msg, options: options).ConfigureAwait(false); + } + + public async Task SendGetGuildsAsync(RequestOptions options = null) + { + return await SendRpcAsync("GET_GUILDS", null, options: options).ConfigureAwait(false); + } + public async Task SendGetGuildAsync(ulong guildId, RequestOptions options = null) + { + var msg = new GetGuildParams + { + GuildId = guildId + }; + return await SendRpcAsync("GET_GUILD", msg, options: options).ConfigureAwait(false); + } + public async Task SendGetChannelsAsync(ulong guildId, RequestOptions options = null) + { + var msg = new GetChannelsParams + { + GuildId = guildId + }; + return await SendRpcAsync("GET_CHANNELS", msg, options: options).ConfigureAwait(false); + } + public async Task SendGetChannelAsync(ulong channelId, RequestOptions options = null) + { + var msg = new GetChannelParams + { + ChannelId = channelId + }; + return await SendRpcAsync("GET_CHANNEL", msg, options: options).ConfigureAwait(false); + } + + public async Task SendSetLocalVolumeAsync(int volume, RequestOptions options = null) + { + var msg = new SetLocalVolumeParams + { + Volume = volume + }; + return await SendRpcAsync("SET_LOCAL_VOLUME", msg, options: options).ConfigureAwait(false); + } + public async Task SendSelectVoiceChannelAsync(ulong channelId, RequestOptions options = null) + { + var msg = new SelectVoiceChannelParams + { + ChannelId = channelId + }; + return await SendRpcAsync("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); + } + + public async Task SendChannelSubscribeAsync(string evt, ulong channelId, RequestOptions options = null) + { + var msg = new ChannelSubscriptionParams + { + ChannelId = channelId + }; + return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendChannelUnsubscribeAsync(string evt, ulong channelId, RequestOptions options = null) + { + var msg = new ChannelSubscriptionParams + { + ChannelId = channelId + }; + return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + + public async Task SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + { + var msg = new GuildSubscriptionParams + { + GuildId = guildId + }; + return await SendRpcAsync("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + public async Task SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) + { + var msg = new GuildSubscriptionParams + { + GuildId = guildId + }; + return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); + } + + private bool ProcessMessage(RpcMessage msg) + { + RpcRequest requestTracker; + if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) + { + if (msg.Event.GetValueOrDefault("") == "ERROR") + { + var _ = requestTracker.SetExceptionAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); + } + else + { + var _ = requestTracker.SetResultAsync(msg.Data.GetValueOrDefault() as JToken, _serializer); + } + return true; + } + else + return false; } //Helpers diff --git a/src/Discord.Net/API/Rpc/Application.cs b/src/Discord.Net/API/Rpc/Application.cs index 1a5520e69..4b4e8350b 100644 --- a/src/Discord.Net/API/Rpc/Application.cs +++ b/src/Discord.Net/API/Rpc/Application.cs @@ -11,7 +11,7 @@ namespace Discord.API.Rpc [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("rpc_origins")] - public string RpcOrigins { get; set; } + public string[] RpcOrigins { get; set; } [JsonProperty("name")] public string Name { get; set; } } diff --git a/src/Discord.Net/API/Rpc/AuthenticateEvent.cs b/src/Discord.Net/API/Rpc/AuthenticateResponse.cs similarity index 91% rename from src/Discord.Net/API/Rpc/AuthenticateEvent.cs rename to src/Discord.Net/API/Rpc/AuthenticateResponse.cs index ca99ce8ff..5723265f6 100644 --- a/src/Discord.Net/API/Rpc/AuthenticateEvent.cs +++ b/src/Discord.Net/API/Rpc/AuthenticateResponse.cs @@ -3,7 +3,7 @@ using System; namespace Discord.API.Rpc { - public class AuthenticateEvent + public class AuthenticateResponse { [JsonProperty("application")] public Application Application { get; set; } diff --git a/src/Discord.Net/API/Rpc/AuthorizeEvent.cs b/src/Discord.Net/API/Rpc/AuthorizeResponse.cs similarity index 81% rename from src/Discord.Net/API/Rpc/AuthorizeEvent.cs rename to src/Discord.Net/API/Rpc/AuthorizeResponse.cs index 8416d5f86..8408c103e 100644 --- a/src/Discord.Net/API/Rpc/AuthorizeEvent.cs +++ b/src/Discord.Net/API/Rpc/AuthorizeResponse.cs @@ -3,7 +3,7 @@ using System; namespace Discord.API.Rpc { - public class AuthorizeEvent + public class AuthorizeResponse { [JsonProperty("code")] public string Code { get; set; } diff --git a/src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs b/src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs new file mode 100644 index 000000000..1432a24de --- /dev/null +++ b/src/Discord.Net/API/Rpc/ChannelSubscriptionParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class ChannelSubscriptionParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetChannelParams.cs b/src/Discord.Net/API/Rpc/GetChannelParams.cs new file mode 100644 index 000000000..4eb882628 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetChannelParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GetChannelParams + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetChannelsParams.cs b/src/Discord.Net/API/Rpc/GetChannelsParams.cs new file mode 100644 index 000000000..fb4e486a5 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetChannelsParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GetChannelsParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetChannelsResponse.cs b/src/Discord.Net/API/Rpc/GetChannelsResponse.cs new file mode 100644 index 000000000..cb331fa49 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetChannelsResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GetChannelsResponse + { + [JsonProperty("channels")] + public RpcChannel[] Channels { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetGuildParams.cs b/src/Discord.Net/API/Rpc/GetGuildParams.cs new file mode 100644 index 000000000..39094e01a --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetGuildParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GetGuildParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GetGuildsParams.cs b/src/Discord.Net/API/Rpc/GetGuildsParams.cs new file mode 100644 index 000000000..cfc2f7ab2 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetGuildsParams.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.API.Rpc +{ + public class GetGuildsParams + { + } +} diff --git a/src/Discord.Net/API/Rpc/GetGuildsResponse.cs b/src/Discord.Net/API/Rpc/GetGuildsResponse.cs new file mode 100644 index 000000000..4df176891 --- /dev/null +++ b/src/Discord.Net/API/Rpc/GetGuildsResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GetGuildsResponse + { + [JsonProperty("guilds")] + public RpcUserGuild[] Guilds { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs b/src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs new file mode 100644 index 000000000..3b66b5c6c --- /dev/null +++ b/src/Discord.Net/API/Rpc/GuildSubscriptionParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class GuildSubscriptionParams + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/MessageEvent.cs b/src/Discord.Net/API/Rpc/MessageEvent.cs new file mode 100644 index 000000000..4aaa87f33 --- /dev/null +++ b/src/Discord.Net/API/Rpc/MessageEvent.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.API.Rpc +{ + public class MessageEvent + { + } +} diff --git a/src/Discord.Net/API/Rpc/RpcChannel.cs b/src/Discord.Net/API/Rpc/RpcChannel.cs new file mode 100644 index 000000000..45d5fd36a --- /dev/null +++ b/src/Discord.Net/API/Rpc/RpcChannel.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class RpcChannel : Channel + { + [JsonProperty("voice_states")] + public VoiceState[] VoiceStates { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/RpcGuild.cs b/src/Discord.Net/API/Rpc/RpcGuild.cs new file mode 100644 index 000000000..70363a93e --- /dev/null +++ b/src/Discord.Net/API/Rpc/RpcGuild.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class RpcGuild : Guild + { + [JsonProperty("online")] + public int Online { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/RpcMessage.cs b/src/Discord.Net/API/Rpc/RpcMessage.cs index 9226e0fa8..73ae3d3a1 100644 --- a/src/Discord.Net/API/Rpc/RpcMessage.cs +++ b/src/Discord.Net/API/Rpc/RpcMessage.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; namespace Discord.API.Rpc { @@ -7,11 +8,11 @@ namespace Discord.API.Rpc [JsonProperty("cmd")] public string Cmd { get; set; } [JsonProperty("nonce")] - public string Nonce { get; set; } + public Optional Nonce { get; set; } [JsonProperty("evt")] - public string Event { get; set; } + public Optional Event { get; set; } [JsonProperty("data")] - public object Data { get; set; } + public Optional Data { get; set; } [JsonProperty("args")] public object Args { get; set; } } diff --git a/src/Discord.Net/API/Rpc/RpcUserGuild.cs b/src/Discord.Net/API/Rpc/RpcUserGuild.cs new file mode 100644 index 000000000..bb538f30d --- /dev/null +++ b/src/Discord.Net/API/Rpc/RpcUserGuild.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class RpcUserGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs b/src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs new file mode 100644 index 000000000..d3ee9336b --- /dev/null +++ b/src/Discord.Net/API/Rpc/SelectVoiceChannelParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class SelectVoiceChannelParams + { + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs b/src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs new file mode 100644 index 000000000..c058ce194 --- /dev/null +++ b/src/Discord.Net/API/Rpc/SetLocalVolumeParams.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class SetLocalVolumeParams + { + [JsonProperty("volume")] + public int Volume { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs b/src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs new file mode 100644 index 000000000..ac75d2697 --- /dev/null +++ b/src/Discord.Net/API/Rpc/SetLocalVolumeResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class SetLocalVolumeResponse + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("volume")] + public int Volume { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/SpeakingEvent.cs b/src/Discord.Net/API/Rpc/SpeakingEvent.cs new file mode 100644 index 000000000..574167b01 --- /dev/null +++ b/src/Discord.Net/API/Rpc/SpeakingEvent.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.API.Rpc +{ + public class SpeakingEvent + { + } +} diff --git a/src/Discord.Net/API/Rpc/SubscriptionResponse.cs b/src/Discord.Net/API/Rpc/SubscriptionResponse.cs new file mode 100644 index 000000000..96c82a546 --- /dev/null +++ b/src/Discord.Net/API/Rpc/SubscriptionResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rpc +{ + public class SubscriptionResponse + { + [JsonProperty("evt")] + public string Event { get; set; } + } +} diff --git a/src/Discord.Net/API/Rpc/VoiceStateEvent.cs b/src/Discord.Net/API/Rpc/VoiceStateEvent.cs new file mode 100644 index 000000000..ce70f02c0 --- /dev/null +++ b/src/Discord.Net/API/Rpc/VoiceStateEvent.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.API.Rpc +{ + public class VoiceStateEvent + { + } +} diff --git a/src/Discord.Net/DiscordRestClient.cs b/src/Discord.Net/DiscordRestClient.cs index 676ba9200..134072253 100644 --- a/src/Discord.Net/DiscordRestClient.cs +++ b/src/Discord.Net/DiscordRestClient.cs @@ -314,7 +314,7 @@ namespace Discord private async Task WriteInitialLog() { if (this is DiscordSocketClient) - await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordConfig.GatewayEncoding})").ConfigureAwait(false); + await _clientLogger.InfoAsync($"DiscordSocketClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, {DiscordSocketConfig.GatewayEncoding})").ConfigureAwait(false); else if (this is DiscordRpcClient) await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordConfig.Version} (API v{DiscordConfig.APIVersion}, RPC API v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); else diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index cfaaed6c4..8467d1f90 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -4,7 +4,6 @@ using Discord.Net.Converters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -48,7 +47,7 @@ namespace Discord public ConnectionState ConnectionState { get; private set; } /// Creates a new RPC discord client. - public DiscordRpcClient(string clientId) : this(new DiscordRpcConfig(clientId)) { } + public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { } /// Creates a new RPC discord client. public DiscordRpcClient(DiscordRpcConfig config) { @@ -59,7 +58,7 @@ namespace Discord _isFirstLogSub = true; _connectionLock = new SemaphoreSlim(1, 1); - + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { @@ -67,7 +66,7 @@ namespace Discord e.ErrorContext.Handled = true; }; - ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.WebSocketProvider); + ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.WebSocketProvider); ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedRpcEvent += ProcessMessageAsync; ApiClient.Disconnected += async ex => @@ -198,11 +197,6 @@ namespace Discord await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); - /*if (_sessionId != null) - await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); - else - await ApiClient.SendIdentifyAsync().ConfigureAwait(false);*/ - await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; @@ -301,25 +295,40 @@ namespace Discord } } - private async Task ProcessMessageAsync(string cmd, string evnt, object payload, string nonce) + private async Task ProcessMessageAsync(string cmd, Optional evnt, Optional payload) { try { switch (cmd) { case "DISPATCH": - switch (evnt) + switch (evnt.Value) { //Connection case "READY": { await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - if (_scopes != null) - await ApiClient.SendAuthorizeAsync(_scopes).ConfigureAwait(false); //No bearer - else - await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); //Has bearer + var data = (payload.Value as JToken).ToObject(_serializer); + var cancelToken = _cancelToken; + + var _ = Task.Run(async () => + { + RequestOptions options = new RequestOptions + { + //CancellationToken = cancelToken //TODO: Implement + }; + + if (_scopes != null) //No bearer + { + var authorizeData = await ApiClient.SendAuthorizeAsync(_scopes, options).ConfigureAwait(false); + await ApiClient.LoginAsync(TokenType.Bearer, authorizeData.Code, options).ConfigureAwait(false); + } + + var authenticateData = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer + + var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + }); } break; @@ -329,32 +338,15 @@ namespace Discord return; } break; - case "AUTHORIZE": - { - await _rpcLogger.DebugAsync("Received AUTHORIZE").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - await ApiClient.LoginAsync(TokenType.Bearer, data.Code).ConfigureAwait(false); - await ApiClient.SendAuthenticateAsync().ConfigureAwait(false); - } - break; - case "AUTHENTICATE": - { - await _rpcLogger.DebugAsync("Received AUTHENTICATE").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete - await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); - } - break; - default: + /*default: await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); - return; + return;*/ } } catch (Exception ex) { - await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt != null ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); return; } } diff --git a/src/Discord.Net/DiscordRpcConfig.cs b/src/Discord.Net/DiscordRpcConfig.cs index b120f8399..7b11d5fc4 100644 --- a/src/Discord.Net/DiscordRpcConfig.cs +++ b/src/Discord.Net/DiscordRpcConfig.cs @@ -9,13 +9,16 @@ namespace Discord public const int PortRangeStart = 6463; public const int PortRangeEnd = 6472; - public DiscordRpcConfig(string clientId) + public DiscordRpcConfig(string clientId, string origin) { ClientId = clientId; + Origin = origin; } /// Gets or sets the Discord client/application id used for this RPC connection. public string ClientId { get; set; } + /// Gets or sets the origin used for this RPC connection. + public string Origin { get; set; } /// Gets or sets the provider used to generate new websocket connections. public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); diff --git a/src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs b/src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs new file mode 100644 index 000000000..f1cdc8203 --- /dev/null +++ b/src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs @@ -0,0 +1,8 @@ +namespace Discord.Entities.Rpc +{ + public interface IRemoteUserGuild : ISnowflakeEntity + { + /// Gets the name of this guild. + string Name { get; } + } +} diff --git a/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs b/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs new file mode 100644 index 000000000..ae4152cd2 --- /dev/null +++ b/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs @@ -0,0 +1,29 @@ +using System; +using Model = Discord.API.Rpc.RpcUserGuild; + +namespace Discord.Entities.Rpc +{ + internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity + { + public ulong Id { get; } + public DiscordRestClient Discord { get; } + public string Name { get; private set; } + + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + + public RemoteUserGuild(DiscordRestClient discord, Model model) + { + Id = model.Id; + Discord = discord; + Update(model, UpdateSource.Creation); + } + public void Update(Model model, UpdateSource source) + { + if (source == UpdateSource.Rest) return; + + Name = model.Name; + } + + bool IEntity.IsAttached => false; + } +} diff --git a/src/Discord.Net/Net/RpcException.cs b/src/Discord.Net/Net/RpcException.cs new file mode 100644 index 000000000..195fad73f --- /dev/null +++ b/src/Discord.Net/Net/RpcException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Discord +{ + public class RpcException : Exception + { + public int ErrorCode { get; } + public string Reason { get; } + + public RpcException(int errorCode, string reason = null) + : base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") + { + ErrorCode = errorCode; + Reason = reason; + } + } +} From 60f3b3f6cbd024f6e736b1841328a41588a20a39 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 05:00:06 -0300 Subject: [PATCH 05/24] Isolated Rpc's AuthorizeAsync --- src/Discord.Net/DiscordRpcClient.cs | 60 +++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index 8467d1f90..97648771b 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -39,7 +39,6 @@ namespace Discord private bool _isFirstLogSub; private bool _isReconnecting; private bool _isDisposed; - private string[] _scopes; public API.DiscordRpcApiClient ApiClient { get; } internal LogManager LogManager { get; } @@ -150,33 +149,21 @@ namespace Discord await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - - /// + public async Task ConnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { _isReconnecting = false; - await ConnectInternalAsync(null).ConfigureAwait(false); + await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - public async Task ConnectAndAuthorizeAsync(params string[] scopes) + private async Task ConnectInternalAsync(bool ignoreLoginCheck = false) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - _isReconnecting = false; - await ConnectInternalAsync(scopes).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task ConnectInternalAsync(string[] scopes) - { - if (scopes == null && LoginState != LoginState.LoggedIn) + if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting or call ConnectAndAuthorizeAsync."); - _scopes = scopes; if (_isFirstLogSub) { @@ -232,8 +219,7 @@ namespace Discord await _rpcLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false); //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); - - _scopes = null; + ConnectionState = ConnectionState.Disconnected; await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false); @@ -272,7 +258,7 @@ namespace Discord await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync(_scopes).ConfigureAwait(false); + await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } return; @@ -295,6 +281,14 @@ namespace Discord } } + public async Task AuthorizeAsync(string[] scopes) + { + await ConnectAsync().ConfigureAwait(false); + var result = await ApiClient.SendAuthorizeAsync(scopes).ConfigureAwait(false); + await DisconnectAsync().ConfigureAwait(false); + return result.Code; + } + private async Task ProcessMessageAsync(string cmd, Optional evnt, Optional payload) { try @@ -313,21 +307,23 @@ namespace Discord var _ = Task.Run(async () => { - RequestOptions options = new RequestOptions + try { - //CancellationToken = cancelToken //TODO: Implement - }; - - if (_scopes != null) //No bearer + RequestOptions options = new RequestOptions + { + //CancellationToken = cancelToken //TODO: Implement + }; + + await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer + + var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete + await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); + } + catch (Exception ex) { - var authorizeData = await ApiClient.SendAuthorizeAsync(_scopes, options).ConfigureAwait(false); - await ApiClient.LoginAsync(TokenType.Bearer, authorizeData.Code, options).ConfigureAwait(false); + await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); + return; } - - var authenticateData = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer - - var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete - await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); }); } break; From b95f2095ef2e758e44f8ebd629ed3d2fa6650e3c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 06:11:03 -0300 Subject: [PATCH 06/24] Further separated Rest/Rpc/Socket logic, added separate improvements from SocketClient --- ...DiscordAPIClient.cs => DiscordRestApiClient.cs} | 269 +++------------------ src/Discord.Net/API/DiscordRpcAPIClient.cs | 156 +++--------- src/Discord.Net/API/DiscordSocketApiClient.cs | 240 ++++++++++++++++++ src/Discord.Net/DiscordRestClient.cs | 59 +++-- src/Discord.Net/DiscordRpcClient.cs | 183 +++++--------- src/Discord.Net/DiscordRpcConfig.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 8 +- src/Discord.Net/Entities/Users/SelfUser.cs | 28 +-- .../Entities/WebSocket/Users/SocketSelfUser.cs | 27 ++- src/Discord.Net/IDiscordClient.cs | 2 +- 10 files changed, 445 insertions(+), 529 deletions(-) rename src/Discord.Net/API/{DiscordAPIClient.cs => DiscordRestApiClient.cs} (81%) create mode 100644 src/Discord.Net/API/DiscordSocketApiClient.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordRestApiClient.cs similarity index 81% rename from src/Discord.Net/API/DiscordAPIClient.cs rename to src/Discord.Net/API/DiscordRestApiClient.cs index ab12fbb9f..90658ef9d 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordRestApiClient.cs @@ -21,91 +21,48 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordApiClient : IDisposable + public class DiscordRestApiClient : IDisposable { - private object _eventLock = new object(); - public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); - public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } - private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); - - public event Func ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } - private readonly AsyncEvent> _receivedGatewayEvent = new AsyncEvent>(); - public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - - private readonly RequestQueue _requestQueue; - private readonly JsonSerializer _serializer; - private readonly IRestClient _restClient; - private readonly IWebSocketClient _gatewayClient; - private readonly SemaphoreSlim _connectionLock; - private CancellationTokenSource _loginCancelToken, _connectCancelToken; - private string _authToken; - private string _gatewayUrl; - private bool _isDisposed; + + protected readonly JsonSerializer _serializer; + protected readonly SemaphoreSlim _stateLock; + private readonly RestClientProvider _restClientProvider; + + protected string _authToken; + protected bool _isDisposed; + private CancellationTokenSource _loginCancelToken; + private IRestClient _restClient; public LoginState LoginState { get; private set; } - public ConnectionState ConnectionState { get; private set; } public TokenType AuthTokenType { get; private set; } + internal RequestQueue RequestQueue { get; private set; } - public DiscordApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider = null, JsonSerializer serializer = null, RequestQueue requestQueue = null) + public DiscordRestApiClient(RestClientProvider restClientProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) { - _connectionLock = new SemaphoreSlim(1, 1); + _restClientProvider = restClientProvider; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + RequestQueue = requestQueue; - _requestQueue = requestQueue ?? new RequestQueue(); + _stateLock = new SemaphoreSlim(1, 1); - _restClient = restClientProvider(DiscordRestConfig.ClientAPIUrl); + SetBaseUrl(DiscordConfig.ClientAPIUrl); + } + internal void SetBaseUrl(string baseUrl) + { + _restClient = _restClientProvider(baseUrl); _restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent); - if (webSocketProvider != null) - { - _gatewayClient = webSocketProvider(); - //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) - _gatewayClient.BinaryMessage += async (data, index, count) => - { - using (var compressed = new MemoryStream(data, index + 2, count - 2)) - using (var decompressed = new MemoryStream()) - { - using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) - zlib.CopyTo(decompressed); - decompressed.Position = 0; - using (var reader = new StreamReader(decompressed)) - using (var jsonReader = new JsonTextReader(reader)) - { - var msg = _serializer.Deserialize(jsonReader); - await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); - } - } - }; - _gatewayClient.TextMessage += async text => - { - using (var reader = new StringReader(text)) - using (var jsonReader = new JsonTextReader(reader)) - { - var msg = _serializer.Deserialize(jsonReader); - await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); - } - }; - _gatewayClient.Closed += async ex => - { - await DisconnectAsync().ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); - }; - } - - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - private void Dispose(bool disposing) + internal virtual void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) { _loginCancelToken?.Dispose(); - _connectCancelToken?.Dispose(); (_restClient as IDisposable)?.Dispose(); - (_gatewayClient as IDisposable)?.Dispose(); } _isDisposed = true; } @@ -114,12 +71,12 @@ namespace Discord.API public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) { - await _connectionLock.WaitAsync().ConfigureAwait(false); + await _stateLock.WaitAsync().ConfigureAwait(false); try { await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); } - finally { _connectionLock.Release(); } + finally { _stateLock.Release(); } } private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) { @@ -134,7 +91,7 @@ namespace Discord.API AuthTokenType = TokenType.User; _authToken = null; _restClient.SetHeader("authorization", null); - await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); + await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); _restClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; @@ -165,12 +122,12 @@ namespace Discord.API public async Task LogoutAsync() { - await _connectionLock.WaitAsync().ConfigureAwait(false); + await _stateLock.WaitAsync().ConfigureAwait(false); try { await LogoutInternalAsync().ConfigureAwait(false); } - finally { _connectionLock.Release(); } + finally { _stateLock.Release(); } } private async Task LogoutInternalAsync() { @@ -182,87 +139,16 @@ namespace Discord.API catch { } await DisconnectInternalAsync().ConfigureAwait(false); - await _requestQueue.ClearAsync().ConfigureAwait(false); + await RequestQueue.ClearAsync().ConfigureAwait(false); - await _requestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); + await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); _restClient.SetCancelToken(CancellationToken.None); LoginState = LoginState.LoggedOut; } - public async Task ConnectAsync() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task ConnectInternalAsync() - { - if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting."); - if (_gatewayClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); - - ConnectionState = ConnectionState.Connecting; - try - { - _connectCancelToken = new CancellationTokenSource(); - if (_gatewayClient != null) - _gatewayClient.SetCancelToken(_connectCancelToken.Token); - - if (_gatewayUrl == null) - { - var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); - _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}"; - } - await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); - - ConnectionState = ConnectionState.Connected; - } - catch (Exception) - { - _gatewayUrl = null; //Uncache in case the gateway url changed - await DisconnectInternalAsync().ConfigureAwait(false); - throw; - } - } - - public async Task DisconnectAsync() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - public async Task DisconnectAsync(Exception ex) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task DisconnectInternalAsync() - { - if (_gatewayClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); - - if (ConnectionState == ConnectionState.Disconnected) return; - ConnectionState = ConnectionState.Disconnecting; - - try { _connectCancelToken?.Cancel(false); } - catch { } - - await _gatewayClient.DisconnectAsync().ConfigureAwait(false); - - ConnectionState = ConnectionState.Disconnected; - } + internal virtual Task ConnectInternalAsync() => Task.CompletedTask; + internal virtual Task DisconnectInternalAsync() => Task.CompletedTask; //REST public Task SendAsync(string method, string endpoint, @@ -306,15 +192,6 @@ namespace Discord.API GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class => DeserializeJson(await SendMultipartInternalAsync(method, endpoint, multipartArgs, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); - //Gateway - public Task SendGatewayAsync(GatewayOpCode opCode, object payload, - GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) - => SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); - - public Task SendGatewayAsync(GatewayOpCode opCode, object payload, - GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); - //Core private async Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) @@ -323,7 +200,7 @@ namespace Discord.API string json = null; if (payload != null) json = SerializeJson(payload); - var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); @@ -335,7 +212,7 @@ namespace Discord.API BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await RequestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); @@ -344,17 +221,6 @@ namespace Discord.API return responseStream; } - private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, - BucketGroup group, int bucketId, ulong guildId, RequestOptions options) - { - //TODO: Add ETF - byte[] bytes = null; - payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; - if (payload != null) - bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); - await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); - } //Auth public async Task ValidateTokenAsync(RequestOptions options = null) @@ -362,69 +228,6 @@ namespace Discord.API await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false); } - //Gateway - public async Task GetGatewayAsync(RequestOptions options = null) - { - return await SendAsync("GET", "gateway", options: options).ConfigureAwait(false); - } - public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) - { - var props = new Dictionary - { - ["$device"] = "Discord.Net" - }; - var msg = new IdentifyParams() - { - Token = _authToken, - Properties = props, - LargeThreshold = largeThreshold, - UseCompression = useCompression - }; - await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); - } - public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) - { - var msg = new ResumeParams() - { - Token = _authToken, - SessionId = sessionId, - Sequence = lastSeq - }; - await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false); - } - public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) - { - await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); - } - public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null) - { - var args = new StatusUpdateParams - { - IdleSince = idleSince, - Game = game - }; - await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); - } - public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) - { - await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); - } - public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null) - { - var payload = new VoiceStateUpdateParams - { - GuildId = guildId, - ChannelId = channelId, - SelfDeaf = selfDeaf, - SelfMute = selfMute - }; - await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); - } - public async Task SendGuildSyncAsync(IEnumerable guildIds, RequestOptions options = null) - { - await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); - } - //Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) { @@ -1230,8 +1033,8 @@ namespace Discord.API } //Helpers - private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - private string SerializeJson(object value) + protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + protected string SerializeJson(object value) { var sb = new StringBuilder(256); using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) @@ -1239,7 +1042,7 @@ namespace Discord.API _serializer.Serialize(writer, value); return sb.ToString(); } - private T DeserializeJson(Stream jsonStream) + protected T DeserializeJson(Stream jsonStream) { using (TextReader text = new StreamReader(jsonStream)) using (JsonReader reader = new JsonTextReader(text)) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 8a813d8e5..49df7d671 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -2,6 +2,7 @@ using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Queue; +using Discord.Net.Rest; using Discord.Net.WebSockets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -17,7 +18,7 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordRpcApiClient : IDisposable + public class DiscordRpcApiClient : DiscordRestApiClient, IDisposable { private abstract class RpcRequest { @@ -60,19 +61,16 @@ namespace Discord.API private readonly ConcurrentDictionary _requests; private readonly RequestQueue _requestQueue; - private readonly JsonSerializer _serializer; private readonly IWebSocketClient _webSocketClient; private readonly SemaphoreSlim _connectionLock; private readonly string _clientId; private CancellationTokenSource _loginCancelToken, _connectCancelToken; - private string _authToken; private string _origin; - private bool _isDisposed; - public LoginState LoginState { get; private set; } public ConnectionState ConnectionState { get; private set; } - public DiscordRpcApiClient(string clientId, string origin, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + public DiscordRpcApiClient(string clientId, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + : base(restClientProvider, serializer, requestQueue) { _connectionLock = new SemaphoreSlim(1, 1); _clientId = clientId; @@ -80,33 +78,19 @@ namespace Discord.API _requestQueue = requestQueue ?? new RequestQueue(); _requests = new ConcurrentDictionary(); - - if (webSocketProvider != null) + + _webSocketClient = webSocketProvider(); + //_webSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _webSocketClient.SetHeader("origin", _origin); + _webSocketClient.BinaryMessage += async (data, index, count) => { - _webSocketClient = webSocketProvider(); - //_webSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) - _webSocketClient.SetHeader("origin", _origin); - _webSocketClient.BinaryMessage += async (data, index, count) => + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) { - using (var compressed = new MemoryStream(data, index + 2, count - 2)) - using (var decompressed = new MemoryStream()) - { - using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) - zlib.CopyTo(decompressed); - decompressed.Position = 0; - using (var reader = new StreamReader(decompressed)) - using (var jsonReader = new JsonTextReader(reader)) - { - var msg = _serializer.Deserialize(jsonReader); - await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); - if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) - ProcessMessage(msg); - } - } - }; - _webSocketClient.TextMessage += async text => - { - using (var reader = new StringReader(text)) + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) using (var jsonReader = new JsonTextReader(reader)) { var msg = _serializer.Deserialize(jsonReader); @@ -114,17 +98,26 @@ namespace Discord.API if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) ProcessMessage(msg); } - }; - _webSocketClient.Closed += async ex => + } + }; + _webSocketClient.TextMessage += async text => + { + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) { - await DisconnectAsync().ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); - }; - } - - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + var msg = _serializer.Deserialize(jsonReader); + await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); + if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) + ProcessMessage(msg); + } + }; + _webSocketClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; } - private void Dispose(bool disposing) + internal override void Dispose(bool disposing) { if (!_isDisposed) { @@ -136,67 +129,6 @@ namespace Discord.API _isDisposed = true; } } - public void Dispose() => Dispose(true); - - public async Task LoginAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternalAsync(tokenType, token, upgrade, options).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LoginInternalAsync(TokenType tokenType, string token, bool upgrade = false, RequestOptions options = null) - { - if (!upgrade && LoginState != LoginState.LoggedOut) - await LogoutInternalAsync().ConfigureAwait(false); - - if (tokenType != TokenType.Bearer) - throw new InvalidOperationException("RPC only supports bearer tokens"); - - LoginState = LoginState.LoggingIn; - try - { - _loginCancelToken = new CancellationTokenSource(); - await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); - - _authToken = token; - - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternalAsync().ConfigureAwait(false); - throw; - } - } - - public async Task LogoutAsync() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LogoutInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LogoutInternalAsync() - { - //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. - if (LoginState == LoginState.LoggedOut) return; - LoginState = LoginState.LoggingOut; - - try { _loginCancelToken?.Cancel(false); } - catch { } - - await DisconnectInternalAsync().ConfigureAwait(false); - await _requestQueue.ClearAsync().ConfigureAwait(false); - - await _requestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); - - LoginState = LoginState.LoggedOut; - } public async Task ConnectAsync() { @@ -207,7 +139,7 @@ namespace Discord.API } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync() + internal override async Task ConnectInternalAsync() { /*if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting.");*/ @@ -226,6 +158,7 @@ namespace Discord.API { string url = $"wss://discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); + SetBaseUrl($"https://discordapp.io:{port}"); success = true; break; } @@ -254,7 +187,7 @@ namespace Discord.API } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync() + internal override async Task DisconnectInternalAsync() { if (_webSocketClient == null) throw new NotSupportedException("This client is not configured with websocket support."); @@ -421,22 +354,5 @@ namespace Discord.API else return false; } - - //Helpers - private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - private string SerializeJson(object value) - { - var sb = new StringBuilder(256); - using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) - using (JsonWriter writer = new JsonTextWriter(text)) - _serializer.Serialize(writer, value); - return sb.ToString(); - } - private T DeserializeJson(Stream jsonStream) - { - using (TextReader text = new StreamReader(jsonStream)) - using (JsonReader reader = new JsonTextReader(text)) - return _serializer.Deserialize(reader); - } } } diff --git a/src/Discord.Net/API/DiscordSocketApiClient.cs b/src/Discord.Net/API/DiscordSocketApiClient.cs new file mode 100644 index 000000000..70d660cd4 --- /dev/null +++ b/src/Discord.Net/API/DiscordSocketApiClient.cs @@ -0,0 +1,240 @@ +using Discord.API.Gateway; +using Discord.API.Rest; +using Discord.Net.Queue; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class DiscordSocketApiClient : DiscordRestApiClient + { + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } + private readonly AsyncEvent> _receivedGatewayEvent = new AsyncEvent>(); + + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly IWebSocketClient _gatewayClient; + private CancellationTokenSource _connectCancelToken; + private string _gatewayUrl; + + public ConnectionState ConnectionState { get; private set; } + + public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null) + : base(restClientProvider, serializer, requestQueue) + { + _gatewayClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _gatewayClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } + } + }; + _gatewayClient.TextMessage += async text => + { + using (var reader = new StringReader(text)) + using (var jsonReader = new JsonTextReader(reader)) + { + var msg = _serializer.Deserialize(jsonReader); + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + } + }; + _gatewayClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + } + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + (_gatewayClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + + public async Task ConnectAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + internal override async Task ConnectInternalAsync() + { + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting."); + if (_gatewayClient == null) + throw new NotSupportedException("This client is not configured with websocket support."); + + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + if (_gatewayClient != null) + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + + if (_gatewayUrl == null) + { + var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); + _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}"; + } + await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + _gatewayUrl = null; //Uncache in case the gateway url changed + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + public async Task DisconnectAsync(Exception ex) + { + await _stateLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _stateLock.Release(); } + } + internal override async Task DisconnectInternalAsync() + { + if (_gatewayClient == null) + throw new NotSupportedException("This client is not configured with websocket support."); + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Core + private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload, + BucketGroup group, int bucketId, ulong guildId, RequestOptions options) + { + //TODO: Add ETF + byte[] bytes = null; + payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + await RequestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); + } + + //Gateway + public Task SendGatewayAsync(GatewayOpCode opCode, object payload, + GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) + => SendGatewayInternalAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); + + public Task SendGatewayAsync(GatewayOpCode opCode, object payload, + GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendGatewayInternalAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); + + public async Task GetGatewayAsync(RequestOptions options = null) + { + return await SendAsync("GET", "gateway", options: options).ConfigureAwait(false); + } + public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) + { + var props = new Dictionary + { + ["$device"] = "Discord.Net" + }; + var msg = new IdentifyParams() + { + Token = _authToken, + Properties = props, + LargeThreshold = largeThreshold, + UseCompression = useCompression + }; + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); + } + public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) + { + var msg = new ResumeParams() + { + Token = _authToken, + SessionId = sessionId, + Sequence = lastSeq + }; + await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false); + } + public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) + { + await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + } + public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null) + { + var args = new StatusUpdateParams + { + IdleSince = idleSince, + Game = game + }; + await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); + } + public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) + { + await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); + } + public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null) + { + var payload = new VoiceStateUpdateParams + { + GuildId = guildId, + ChannelId = channelId, + SelfDeaf = selfDeaf, + SelfMute = selfMute + }; + await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); + } + public async Task SendGuildSyncAsync(IEnumerable guildIds, RequestOptions options = null) + { + await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/DiscordRestClient.cs b/src/Discord.Net/DiscordRestClient.cs index 134072253..92c27e3da 100644 --- a/src/Discord.Net/DiscordRestClient.cs +++ b/src/Discord.Net/DiscordRestClient.cs @@ -27,20 +27,21 @@ namespace Discord internal readonly ILogger _clientLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; - internal readonly RequestQueue _requestQueue; internal SelfUser _currentUser; private bool _isFirstLogSub; internal bool _isDisposed; - public API.DiscordApiClient ApiClient { get; } + public API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } /// Creates a new REST-only discord client. public DiscordRestClient() : this(new DiscordRestConfig()) { } + public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { } /// Creates a new REST-only discord client. - public DiscordRestClient(DiscordRestConfig config) + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient client) { + ApiClient = client; LogManager = new LogManager(config.LogLevel); LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); _clientLogger = LogManager.CreateLogger("Client"); @@ -50,19 +51,16 @@ namespace Discord _connectionLock = new SemaphoreSlim(1, 1); - _requestQueue = new RequestQueue(); - _requestQueue.RateLimitTriggered += async (id, bucket, millis) => + ApiClient.RequestQueue.RateLimitTriggered += async (id, bucket, millis) => { await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); if (bucket == null && id != null) await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); }; - - var restProvider = config.RestClientProvider; - var webSocketProvider = (this is DiscordSocketClient) ? (config as DiscordSocketConfig)?.WebSocketProvider : null; //TODO: Clean this check - ApiClient = new API.DiscordApiClient(restProvider, webSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, requestQueue: new RequestQueue()); /// public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) @@ -89,27 +87,9 @@ namespace Discord try { await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); - if (validateToken) - { - try - { - var user = await GetCurrentUserAsync().ConfigureAwait(false); - if (user == null) //Is using a cached DiscordClient - user = new SelfUser(this, await ApiClient.GetMyUserAsync().ConfigureAwait(false)); - - if (user.IsBot && tokenType == TokenType.User) - throw new InvalidOperationException($"A bot token used provided with {nameof(TokenType)}.{nameof(TokenType.User)}"); - else if (!user.IsBot && tokenType == TokenType.Bot) //Discord currently sends a 401 in this case - throw new InvalidOperationException($"A user token used provided with {nameof(TokenType)}.{nameof(TokenType.Bot)}"); - } - catch (HttpException ex) - { - throw new ArgumentException("Token validation failed", nameof(token), ex); - } - } - - await OnLoginAsync().ConfigureAwait(false); + await ValidateTokenAsync(tokenType, token).ConfigureAwait(false); + await OnLoginAsync(tokenType, token).ConfigureAwait(false); LoginState = LoginState.LoggedIn; } @@ -121,7 +101,26 @@ namespace Discord await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - protected virtual Task OnLoginAsync() => Task.CompletedTask; + protected virtual async Task ValidateTokenAsync(TokenType tokenType, string token) + { + try + { + var user = await GetCurrentUserAsync().ConfigureAwait(false); + if (user == null) //Is using a cached DiscordClient + user = new SelfUser(this, await ApiClient.GetMyUserAsync().ConfigureAwait(false)); + + if (user.IsBot && tokenType == TokenType.User) + throw new InvalidOperationException($"A bot token used provided with {nameof(TokenType)}.{nameof(TokenType.User)}"); + else if (!user.IsBot && tokenType == TokenType.Bot) //Discord currently sends a 401 in this case + throw new InvalidOperationException($"A user token used provided with {nameof(TokenType)}.{nameof(TokenType.Bot)}"); + } + catch (HttpException ex) + { + throw new ArgumentException("Token validation failed", nameof(token), ex); + } + } + protected virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.CompletedTask; + /// public async Task LogoutAsync() diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index 97648771b..6d3f2ac64 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -1,6 +1,7 @@ using Discord.API.Rpc; using Discord.Logging; using Discord.Net.Converters; +using Discord.Net.Queue; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -10,16 +11,8 @@ using System.Threading.Tasks; namespace Discord { - public class DiscordRpcClient + public class DiscordRpcClient : DiscordRestClient { - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - private readonly AsyncEvent> _logEvent = new AsyncEvent>(); - - public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } - private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); - public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } - private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } @@ -28,44 +21,36 @@ namespace Discord public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); - private readonly ILogger _clientLogger, _rpcLogger; - private readonly SemaphoreSlim _connectionLock; + private readonly ILogger _rpcLogger; private readonly JsonSerializer _serializer; private TaskCompletionSource _connectTask; - private CancellationTokenSource _cancelToken; - internal SelfUser _currentUser; + private CancellationTokenSource _cancelToken, _reconnectCancelToken; private Task _reconnectTask; private bool _isFirstLogSub; private bool _isReconnecting; - private bool _isDisposed; + private bool _canReconnect; - public API.DiscordRpcApiClient ApiClient { get; } - internal LogManager LogManager { get; } - public LoginState LoginState { get; private set; } public ConnectionState ConnectionState { get; private set; } + public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; + /// Creates a new RPC discord client. public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { } /// Creates a new RPC discord client. public DiscordRpcClient(DiscordRpcConfig config) + : base(config, CreateApiClient(config)) { - LogManager = new LogManager(config.LogLevel); - LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _clientLogger = LogManager.CreateLogger("Client"); _rpcLogger = LogManager.CreateLogger("RPC"); _isFirstLogSub = true; - _connectionLock = new SemaphoreSlim(1, 1); - _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { _rpcLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); e.ErrorContext.Handled = true; }; - - ApiClient = new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.WebSocketProvider); + ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedRpcEvent += ProcessMessageAsync; ApiClient.Disconnected += async ex => @@ -79,91 +64,48 @@ namespace Discord await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; } - private void Dispose(bool disposing) + private static API.DiscordRpcApiClient CreateApiClient(DiscordRpcConfig config) + => new API.DiscordRpcApiClient(config.ClientId, config.Origin, config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); + + internal override void Dispose(bool disposing) { if (!_isDisposed) - { ApiClient.Dispose(); - _isDisposed = true; - } } - public void Dispose() => Dispose(true); - - /// - public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) + + protected override async Task OnLoginAsync(TokenType tokenType, string token) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); } - private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) + protected override async Task OnLogoutAsync() { - if (_isFirstLogSub) - { - _isFirstLogSub = false; - await WriteInitialLog().ConfigureAwait(false); - } - - if (LoginState != LoginState.LoggedOut) - await LogoutInternalAsync().ConfigureAwait(false); - LoginState = LoginState.LoggingIn; - - try - { - await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); - - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternalAsync().ConfigureAwait(false); - throw; - } - - await _loggedInEvent.InvokeAsync().ConfigureAwait(false); + await ApiClient.LogoutAsync().ConfigureAwait(false); } - /// - public async Task LogoutAsync() + protected override Task ValidateTokenAsync(TokenType tokenType, string token) { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LogoutInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + return Task.CompletedTask; //Validation is done in DiscordRpcAPIClient } - private async Task LogoutInternalAsync() - { - if (LoginState == LoginState.LoggedOut) return; - LoginState = LoginState.LoggingOut; - - await ApiClient.LogoutAsync().ConfigureAwait(false); - - _currentUser = null; - - LoginState = LoginState.LoggedOut; - await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); - } - - public async Task ConnectAsync() + /// + public Task ConnectAsync() => ConnectAsync(false); + internal async Task ConnectAsync(bool ignoreLoginCheck) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { _isReconnecting = false; - await ConnectInternalAsync().ConfigureAwait(false); + await ConnectInternalAsync(ignoreLoginCheck, false).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(bool ignoreLoginCheck = false) + private async Task ConnectInternalAsync(bool ignoreLoginCheck, bool isReconnecting) { - if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting or call ConnectAndAuthorizeAsync."); + if (!ignoreLoginCheck && LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting."); + + if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) + _reconnectCancelToken.Cancel(); if (_isFirstLogSub) { @@ -173,7 +115,7 @@ namespace Discord var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync(null).ConfigureAwait(false); + await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; await _rpcLogger.InfoAsync("Connecting").ConfigureAwait(false); @@ -185,13 +127,13 @@ namespace Discord await _connectedEvent.InvokeAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); - + _canReconnect = true; ConnectionState = ConnectionState.Connected; await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false); } catch (Exception) { - await DisconnectInternalAsync(null).ConfigureAwait(false); + await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); throw; } } @@ -202,12 +144,20 @@ namespace Discord try { _isReconnecting = false; - await DisconnectInternalAsync(null).ConfigureAwait(false); + await DisconnectInternalAsync(null, false).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync(Exception ex) + private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting) { + if (!isReconnecting) + { + _canReconnect = false; + + if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) + _reconnectCancelToken.Cancel(); + } + if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; await _rpcLogger.InfoAsync("Disconnecting").ConfigureAwait(false); @@ -228,53 +178,51 @@ namespace Discord private async Task StartReconnectAsync(Exception ex) { - //TODO: Is this thread-safe? - if (_reconnectTask != null) return; - + _connectTask?.TrySetException(ex); await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(ex).ConfigureAwait(false); - if (_reconnectTask != null) return; - _isReconnecting = true; - _reconnectTask = ReconnectInternalAsync(); + if (!_canReconnect || _reconnectTask != null) return; + await DisconnectInternalAsync(null, true).ConfigureAwait(false); + _reconnectCancelToken = new CancellationTokenSource(); + _reconnectTask = ReconnectInternalAsync(_reconnectCancelToken.Token); } finally { _connectionLock.Release(); } } - private async Task ReconnectInternalAsync() + private async Task ReconnectInternalAsync(CancellationToken cancelToken) { try { + Random jitter = new Random(); int nextReconnectDelay = 1000; - while (_isReconnecting) + while (true) { + await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false); + nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250); + if (nextReconnectDelay > 60000) + nextReconnectDelay = 60000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await Task.Delay(nextReconnectDelay).ConfigureAwait(false); - nextReconnectDelay *= 2; - if (nextReconnectDelay > 30000) - nextReconnectDelay = 30000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } + if (cancelToken.IsCancellationRequested) return; + await ConnectInternalAsync(false, true).ConfigureAwait(false); + _reconnectTask = null; return; } catch (Exception ex) { await _rpcLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); } + finally { _connectionLock.Release(); } } } - finally + catch (OperationCanceledException) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _isReconnecting = false; + await _rpcLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false); _reconnectTask = null; } finally { _connectionLock.Release(); } @@ -283,7 +231,7 @@ namespace Discord public async Task AuthorizeAsync(string[] scopes) { - await ConnectAsync().ConfigureAwait(false); + await ConnectAsync(true).ConfigureAwait(false); var result = await ApiClient.SendAuthorizeAsync(scopes).ConfigureAwait(false); await DisconnectAsync().ConfigureAwait(false); return result.Code; @@ -314,7 +262,8 @@ namespace Discord //CancellationToken = cancelToken //TODO: Implement }; - await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer + if (LoginState != LoginState.LoggedOut) + await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false); diff --git a/src/Discord.Net/DiscordRpcConfig.cs b/src/Discord.Net/DiscordRpcConfig.cs index 7b11d5fc4..c7e38bd44 100644 --- a/src/Discord.Net/DiscordRpcConfig.cs +++ b/src/Discord.Net/DiscordRpcConfig.cs @@ -2,7 +2,7 @@ namespace Discord { - public class DiscordRpcConfig : DiscordConfig + public class DiscordRpcConfig : DiscordRestConfig { public const int RpcAPIVersion = 1; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index ddad2a613..3d485d7e5 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -3,6 +3,7 @@ using Discord.Audio; using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; +using Discord.Net.Queue; using Discord.Net.WebSockets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -52,6 +53,7 @@ namespace Discord internal DataStore DataStore { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } + public new API.DiscordSocketApiClient ApiClient => base.ApiClient as API.DiscordSocketApiClient; internal SocketSelfUser CurrentUser => _currentUser as SocketSelfUser; internal IReadOnlyCollection Guilds => DataStore.Guilds; internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); @@ -60,7 +62,7 @@ namespace Discord public DiscordSocketClient() : this(new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket discord client. public DiscordSocketClient(DiscordSocketConfig config) - : base(config) + : base(config, CreateApiClient(config)) { ShardId = config.ShardId; TotalShards = config.TotalShards; @@ -106,8 +108,10 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } + private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, requestQueue: new RequestQueue()); - protected override async Task OnLoginAsync() + protected override async Task OnLoginAsync(TokenType tokenType, string token) { var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index dca9ae837..32576b3e0 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -7,9 +7,9 @@ namespace Discord { internal class SelfUser : User, ISelfUser { - private long _idleSince; - private UserStatus _status; - private Game _game; + protected long _idleSince; + protected UserStatus _status; + protected Game _game; public string Email { get; private set; } public bool IsVerified { get; private set; } @@ -61,27 +61,7 @@ namespace Discord var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task ModifyStatusAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyPresenceParams(); - func(args); - var game = args._game.GetValueOrDefault(_game); - var status = args._status.GetValueOrDefault(_status); - - long idleSince = _idleSince; - if (status == UserStatus.Idle && _status != UserStatus.Idle) - idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null; - - await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false); - - //Save values - _idleSince = idleSince; - _game = game; - _status = status; - } + Task ISelfUser.ModifyStatusAsync(Action func) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs index a4a6aa733..4aceb62ab 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs @@ -1,4 +1,6 @@ -using System; +using Discord.API.Rest; +using System; +using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord @@ -15,6 +17,29 @@ namespace Discord { } + public async Task ModifyStatusAsync(Action func) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var args = new ModifyPresenceParams(); + func(args); + + var game = args._game.GetValueOrDefault(_game); + var status = args._status.GetValueOrDefault(_status); + + long idleSince = _idleSince; + if (status == UserStatus.Idle && _status != UserStatus.Idle) + idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var apiGame = game != null ? new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl } : null; + + await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false); + + //Save values + _idleSince = idleSince; + _game = game; + _status = status; + } + public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; ISocketUser ISocketUser.Clone() => Clone(); } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 87034d2c1..263191099 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -13,7 +13,7 @@ namespace Discord { ConnectionState ConnectionState { get; } - DiscordApiClient ApiClient { get; } + DiscordRestApiClient ApiClient { get; } ILogManager LogManager { get; } Task ConnectAsync(); From 03e5b9a8d59e65d4613f58bf08b37bf02892c2b5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 06:28:13 -0300 Subject: [PATCH 07/24] Fixed bad RPC proxy url --- src/Discord.Net/API/DiscordRpcAPIClient.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 49df7d671..923371c45 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -1,6 +1,4 @@ using Discord.API.Rpc; -using Discord.Logging; -using Discord.Net.Converters; using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; @@ -8,8 +6,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; -using System.Diagnostics; -using System.Globalization; using System.IO; using System.IO.Compression; using System.Text; @@ -158,7 +154,7 @@ namespace Discord.API { string url = $"wss://discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); - SetBaseUrl($"https://discordapp.io:{port}"); + SetBaseUrl($"https://discordapp.io:{port}/"); success = true; break; } From c8db35cc4e3a87208b1ede9be682f2967e773f2f Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 16:52:28 -0300 Subject: [PATCH 08/24] Fixed encoding error when requesting members --- src/Discord.Net/API/Gateway/RequestMembersParams.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs index 81e83f5c1..10f98030c 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -13,7 +13,7 @@ namespace Discord.API.Gateway public int Limit { get; set; } [JsonProperty("guild_id")] - private ulong[] _guildIds; + private ulong[] _guildIds { get; set; } public IEnumerable GuildIds { set { _guildIds = value.ToArray(); } } public IEnumerable Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } } } From e4569f1a3922e8eff8beff5b4884500285de89ee Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 21:39:43 -0300 Subject: [PATCH 09/24] Added RpcToken param --- src/Discord.Net/API/DiscordRpcAPIClient.cs | 15 ++++++++++----- src/Discord.Net/API/Rpc/AuthorizeParams.cs | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 923371c45..6b1256cbb 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -148,13 +148,15 @@ namespace Discord.API _webSocketClient.SetCancelToken(_connectCancelToken.Token); bool success = false; - for (int port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++) + int port; + string uuid = Guid.NewGuid().ToString(); + + for ( port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++) { try { - string url = $"wss://discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; + string url = $"wss://{uuid}.discordapp.io:{port}/?v={DiscordRpcConfig.RpcAPIVersion}&client_id={_clientId}"; await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); - SetBaseUrl($"https://discordapp.io:{port}/"); success = true; break; } @@ -162,9 +164,11 @@ namespace Discord.API { } } + if (!success) throw new Exception("Unable to connect to the RPC server."); + SetBaseUrl($"https://{uuid}.discordapp.io:{port}/"); ConnectionState = ConnectionState.Connected; } catch (Exception) @@ -238,12 +242,13 @@ namespace Discord.API }; return await SendRpcAsync("AUTHENTICATE", msg, options: options).ConfigureAwait(false); } - public async Task SendAuthorizeAsync(string[] scopes, RequestOptions options = null) + public async Task SendAuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) { var msg = new AuthorizeParams() { ClientId = _clientId, - Scopes = scopes + Scopes = scopes, + RpcToken = rpcToken != null ? rpcToken : Optional.Create() }; if (options == null) options = new RequestOptions(); diff --git a/src/Discord.Net/API/Rpc/AuthorizeParams.cs b/src/Discord.Net/API/Rpc/AuthorizeParams.cs index bf8bd162a..a16664861 100644 --- a/src/Discord.Net/API/Rpc/AuthorizeParams.cs +++ b/src/Discord.Net/API/Rpc/AuthorizeParams.cs @@ -8,5 +8,7 @@ namespace Discord.API.Rpc public string ClientId { get; set; } [JsonProperty("scopes")] public string[] Scopes { get; set; } + [JsonProperty("rpc_token")] + public Optional RpcToken { get; set; } } } From 70ad0f9a66d65fb88f63b2c443130e06d062c4a8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 21:40:37 -0300 Subject: [PATCH 10/24] Fixed RPC proxied requests --- src/Discord.Net/API/DiscordRestApiClient.cs | 31 +++++++++++++++-------------- src/Discord.Net/DiscordRpcClient.cs | 9 --------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Discord.Net/API/DiscordRestApiClient.cs b/src/Discord.Net/API/DiscordRestApiClient.cs index 90658ef9d..ae208629e 100644 --- a/src/Discord.Net/API/DiscordRestApiClient.cs +++ b/src/Discord.Net/API/DiscordRestApiClient.cs @@ -54,6 +54,21 @@ namespace Discord.API _restClient = _restClientProvider(baseUrl); _restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent); + _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); + } + internal static string GetPrefixedToken(TokenType tokenType, string token) + { + switch (tokenType) + { + case TokenType.Bot: + return $"Bot {token}"; + case TokenType.Bearer: + return $"Bearer {token}"; + case TokenType.User: + return token; + default: + throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); + } } internal virtual void Dispose(bool disposing) { @@ -90,26 +105,12 @@ namespace Discord.API AuthTokenType = TokenType.User; _authToken = null; - _restClient.SetHeader("authorization", null); await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); _restClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; _authToken = token; - 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); + _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); LoginState = LoginState.LoggedIn; } diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index 6d3f2ac64..17800033e 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -72,15 +72,6 @@ namespace Discord if (!_isDisposed) ApiClient.Dispose(); } - - protected override async Task OnLoginAsync(TokenType tokenType, string token) - { - await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); - } - protected override async Task OnLogoutAsync() - { - await ApiClient.LogoutAsync().ConfigureAwait(false); - } protected override Task ValidateTokenAsync(TokenType tokenType, string token) { From 0fc771cc5c103a148feaa8a9b1a9101e05e7d564 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 21:43:46 -0300 Subject: [PATCH 11/24] Add rpcToken to DiscordRpcClient.AuthorizeAsync --- src/Discord.Net/API/DiscordRpcAPIClient.cs | 4 ++-- src/Discord.Net/DiscordRpcClient.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 6b1256cbb..409994ecf 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -236,7 +236,7 @@ namespace Discord.API //Rpc public async Task SendAuthenticateAsync(RequestOptions options = null) { - var msg = new AuthenticateParams() + var msg = new AuthenticateParams { AccessToken = _authToken }; @@ -244,7 +244,7 @@ namespace Discord.API } public async Task SendAuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) { - var msg = new AuthorizeParams() + var msg = new AuthorizeParams { ClientId = _clientId, Scopes = scopes, diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index 17800033e..d73473d20 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -220,10 +220,10 @@ namespace Discord } } - public async Task AuthorizeAsync(string[] scopes) + public async Task AuthorizeAsync(string[] scopes, string rpcToken = null) { await ConnectAsync(true).ConfigureAwait(false); - var result = await ApiClient.SendAuthorizeAsync(scopes).ConfigureAwait(false); + var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken).ConfigureAwait(false); await DisconnectAsync().ConfigureAwait(false); return result.Code; } From ff017bd51f886d7e7d60ac5b7cf5057fcf0464d3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 28 Jul 2016 23:20:36 -0300 Subject: [PATCH 12/24] Remove string check for unparsed --- src/Discord.Net.Commands/Command.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 909765e93..9b8ed98e1 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -88,13 +88,8 @@ namespace Discord.Commands throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); bool isUnparsed = parameter.GetCustomAttribute() != null; - if (isUnparsed) - { - if (type != typeof(string)) - throw new InvalidOperationException("Unparsed parameters only support the string type."); - else if (i != parameters.Length - 1) - throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command."); - } + if (isUnparsed && i != parameters.Length - 1) + throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command."); string name = parameter.Name; string description = typeInfo.GetCustomAttribute()?.Text; From 9b34e4f2b38464e53f0a76eb6932ad7d0843401a Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 29 Jul 2016 10:18:31 -0700 Subject: [PATCH 13/24] Null check wasn't picking up the slack when InformationVersion is not found. --- src/Discord.Net/DiscordConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 2f0f50018..fdecde6b4 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -5,7 +5,7 @@ namespace Discord public class DiscordConfig { public const int APIVersion = 6; - public static string Version { get; } = typeof(DiscordRestConfig).GetTypeInfo().Assembly?.GetCustomAttribute().InformationalVersion ?? "Unknown"; + public static string Version { get; } = typeof(DiscordRestConfig).GetTypeInfo().Assembly?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public const string CDNUrl = "https://cdn.discordapp.com/"; From 80bfe563225a701bcc5e6f72eede536d8694ce74 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 29 Jul 2016 10:25:15 -0700 Subject: [PATCH 14/24] Added Sharding support --- src/Discord.Net/API/DiscordSocketApiClient.cs | 6 ++++-- src/Discord.Net/API/Gateway/IdentifyParams.cs | 2 ++ src/Discord.Net/DiscordSocketClient.cs | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/API/DiscordSocketApiClient.cs b/src/Discord.Net/API/DiscordSocketApiClient.cs index 70d660cd4..f6040081e 100644 --- a/src/Discord.Net/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net/API/DiscordSocketApiClient.cs @@ -179,7 +179,7 @@ namespace Discord.API { return await SendAsync("GET", "gateway", options: options).ConfigureAwait(false); } - public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) + public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null) { var props = new Dictionary { @@ -190,8 +190,10 @@ namespace Discord.API Token = _authToken, Properties = props, LargeThreshold = largeThreshold, - UseCompression = useCompression + UseCompression = useCompression, + ShardingParams = totalShards > 1 ? new int[] { shardID, totalShards } : null }; + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net/API/Gateway/IdentifyParams.cs index 0a056e225..26e2952a1 100644 --- a/src/Discord.Net/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net/API/Gateway/IdentifyParams.cs @@ -14,5 +14,7 @@ namespace Discord.API.Gateway public int LargeThreshold { get; set; } [JsonProperty("compress")] public bool UseCompression { get; set; } + [JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)] + public int[] ShardingParams { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 3d485d7e5..72d48079f 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -166,7 +166,7 @@ namespace Discord if (_sessionId != null) await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); else - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards:TotalShards).ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); _canReconnect = true; @@ -534,7 +534,7 @@ namespace Discord _sessionId = null; _lastSeq = 0; - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: From 4e75b086e17319617f3727302c8cfcbe80577937 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 29 Jul 2016 12:09:50 -0700 Subject: [PATCH 15/24] Using Optional now. --- src/Discord.Net/API/DiscordSocketApiClient.cs | 3 ++- src/Discord.Net/API/Gateway/IdentifyParams.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/API/DiscordSocketApiClient.cs b/src/Discord.Net/API/DiscordSocketApiClient.cs index f6040081e..be149607d 100644 --- a/src/Discord.Net/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net/API/DiscordSocketApiClient.cs @@ -191,8 +191,9 @@ namespace Discord.API Properties = props, LargeThreshold = largeThreshold, UseCompression = useCompression, - ShardingParams = totalShards > 1 ? new int[] { shardID, totalShards } : null }; + if (totalShards > 1) + msg.ShardingParams = new int[] { shardID, totalShards }; await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net/API/Gateway/IdentifyParams.cs index 26e2952a1..2681b6708 100644 --- a/src/Discord.Net/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net/API/Gateway/IdentifyParams.cs @@ -14,7 +14,7 @@ namespace Discord.API.Gateway public int LargeThreshold { get; set; } [JsonProperty("compress")] public bool UseCompression { get; set; } - [JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)] - public int[] ShardingParams { get; set; } + [JsonProperty("shard")] + public Optional ShardingParams { get; set; } } } From 848ffc71adcc3e58cc222b6742a0e22dba0bd48e Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 16:30:55 -0300 Subject: [PATCH 16/24] Cleaned up DiscordConfig.Version --- src/Discord.Net/DiscordConfig.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index fdecde6b4..88cc96db2 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -5,7 +5,10 @@ namespace Discord public class DiscordConfig { public const int APIVersion = 6; - public static string Version { get; } = typeof(DiscordRestConfig).GetTypeInfo().Assembly?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + public static string Version { get; } = + typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? + typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? + "Unknown"; public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public const string CDNUrl = "https://cdn.discordapp.com/"; From 73d4233684fb4bf2a541af004bc68b7e7aee74ea Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 16:46:30 -0300 Subject: [PATCH 17/24] Removed DiscordRpcClient's second log header --- src/Discord.Net/DiscordRpcClient.cs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/DiscordRpcClient.cs index d73473d20..453ca4ac3 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/DiscordRpcClient.cs @@ -27,7 +27,6 @@ namespace Discord private TaskCompletionSource _connectTask; private CancellationTokenSource _cancelToken, _reconnectCancelToken; private Task _reconnectTask; - private bool _isFirstLogSub; private bool _isReconnecting; private bool _canReconnect; @@ -42,7 +41,6 @@ namespace Discord : base(config, CreateApiClient(config)) { _rpcLogger = LogManager.CreateLogger("RPC"); - _isFirstLogSub = true; _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => @@ -98,12 +96,6 @@ namespace Discord if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) _reconnectCancelToken.Cancel(); - if (_isFirstLogSub) - { - _isFirstLogSub = false; - await WriteInitialLog().ConfigureAwait(false); - } - var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); @@ -286,22 +278,5 @@ namespace Discord return; } } - - private async Task WriteInitialLog() - { - await _clientLogger.InfoAsync($"DiscordRpcClient v{DiscordRestConfig.Version} (RPC v{DiscordRpcConfig.RpcAPIVersion})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); - await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); - } - private static string ToArchString(Architecture arch) - { - switch (arch) - { - case Architecture.X64: return "x64"; - case Architecture.X86: return "x86"; - default: return arch.ToString(); - } - } } } From c565bdf55bf44da6961d3aeb650c43dc4ec92290 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 17:12:58 -0300 Subject: [PATCH 18/24] Reorganized classes into Rpc/Rest/WebSocket namespaces --- src/Discord.Net/API/CDN.cs | 10 ++++---- src/Discord.Net/API/DiscordRestApiClient.cs | 29 +++++++++++----------- src/Discord.Net/API/DiscordRpcAPIClient.cs | 1 + .../API/Rest/GetChannelMessagesParams.cs | 6 +++-- src/Discord.Net/Audio/AudioClient.cs | 1 + src/Discord.Net/{Data => }/DataStore.cs | 3 +-- src/Discord.Net/DiscordConfig.cs | 4 +++ src/Discord.Net/Entities/Application.cs | 3 ++- src/Discord.Net/Entities/Channels/DMChannel.cs | 1 + src/Discord.Net/Entities/Channels/GroupChannel.cs | 2 +- src/Discord.Net/Entities/Channels/GuildChannel.cs | 1 + .../Entities/Channels/IMessageChannel.cs | 4 +-- src/Discord.Net/Entities/Entity.cs | 4 ++- src/Discord.Net/Entities/Guilds/Guild.cs | 2 +- .../Entities/Guilds/GuildIntegration.cs | 1 + src/Discord.Net/Entities/Guilds/UserGuild.cs | 3 ++- src/Discord.Net/Entities/Invites/Invite.cs | 7 +++--- src/Discord.Net/Entities/Invites/InviteMetadata.cs | 3 ++- src/Discord.Net/Entities/Messages/Message.cs | 1 + src/Discord.Net/Entities/Roles/Role.cs | 4 +-- src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs | 3 ++- src/Discord.Net/Entities/Users/GroupUser.cs | 1 + src/Discord.Net/Entities/Users/GuildUser.cs | 1 + src/Discord.Net/Entities/Users/SelfUser.cs | 1 + src/Discord.Net/Entities/Users/User.cs | 3 ++- .../Entities/WebSocket/Channels/MessageCache.cs | 5 ++-- .../Entities/WebSocket/Channels/MessageManager.cs | 4 ++- .../Entities/WebSocket/Channels/SocketDMChannel.cs | 3 ++- .../WebSocket/Channels/SocketGroupChannel.cs | 2 +- .../WebSocket/Channels/SocketTextChannel.cs | 8 +++--- .../WebSocket/Channels/SocketVoiceChannel.cs | 1 + .../Entities/WebSocket/Guilds/SocketGuild.cs | 2 +- .../Entities/WebSocket/Messages/SocketMessage.cs | 3 ++- .../Entities/WebSocket/Users/SocketDMUser.cs | 3 ++- .../Entities/WebSocket/Users/SocketGlobalUser.cs | 3 ++- .../Entities/WebSocket/Users/SocketGroupUser.cs | 3 ++- .../Entities/WebSocket/Users/SocketGuildUser.cs | 3 ++- .../Entities/WebSocket/Users/SocketSelfUser.cs | 1 + src/Discord.Net/Extensions/CollectionExtensions.cs | 2 +- .../Extensions/DiscordClientExtensions.cs | 5 ++-- src/Discord.Net/Extensions/GuildExtensions.cs | 2 +- src/Discord.Net/Extensions/GuildUserExtensions.cs | 2 +- src/Discord.Net/{ => Rest}/DiscordRestClient.cs | 4 ++- src/Discord.Net/{ => Rest}/DiscordRestConfig.cs | 8 ++---- src/Discord.Net/{ => Rpc}/DiscordRpcClient.cs | 13 +++------- src/Discord.Net/{ => Rpc}/DiscordRpcConfig.cs | 3 ++- src/Discord.Net/Rpc/RpcEvents.cs | 6 +++++ .../{ => WebSocket}/DiscordSocketClient.Events.cs | 2 +- .../{ => WebSocket}/DiscordSocketClient.cs | 4 +-- .../{ => WebSocket}/DiscordSocketConfig.cs | 1 + 50 files changed, 113 insertions(+), 79 deletions(-) rename src/Discord.Net/{Data => }/DataStore.cs (99%) rename src/Discord.Net/{ => Rest}/DiscordRestClient.cs (99%) rename src/Discord.Net/{ => Rest}/DiscordRestConfig.cs (77%) rename src/Discord.Net/{ => Rpc}/DiscordRpcClient.cs (93%) rename src/Discord.Net/{ => Rpc}/DiscordRpcConfig.cs (95%) create mode 100644 src/Discord.Net/Rpc/RpcEvents.cs rename src/Discord.Net/{ => WebSocket}/DiscordSocketClient.Events.cs (99%) rename src/Discord.Net/{ => WebSocket}/DiscordSocketClient.cs (99%) rename src/Discord.Net/{ => WebSocket}/DiscordSocketConfig.cs (98%) diff --git a/src/Discord.Net/API/CDN.cs b/src/Discord.Net/API/CDN.cs index 19c6e5b3c..3973344db 100644 --- a/src/Discord.Net/API/CDN.cs +++ b/src/Discord.Net/API/CDN.cs @@ -3,14 +3,14 @@ internal static class CDN { public static string GetApplicationIconUrl(ulong appId, string iconId) - => iconId != null ? $"{DiscordRestConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; + => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; public static string GetUserAvatarUrl(ulong userId, string avatarId) - => avatarId != null ? $"{DiscordRestConfig.CDNUrl}avatars/{userId}/{avatarId}.jpg" : null; + => avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.jpg" : null; public static string GetGuildIconUrl(ulong guildId, string iconId) - => iconId != null ? $"{DiscordRestConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; + => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; public static string GetGuildSplashUrl(ulong guildId, string splashId) - => splashId != null ? $"{DiscordRestConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; + => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) - => iconId != null ? $"{DiscordRestConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; + => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; } } diff --git a/src/Discord.Net/API/DiscordRestApiClient.cs b/src/Discord.Net/API/DiscordRestApiClient.cs index ae208629e..9136399b0 100644 --- a/src/Discord.Net/API/DiscordRestApiClient.cs +++ b/src/Discord.Net/API/DiscordRestApiClient.cs @@ -5,6 +5,7 @@ using Discord.Net.Converters; using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; +using Discord.Rest; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -594,13 +595,13 @@ namespace Discord.API List result; if (args._limit.IsSpecified) - result = new List((limit + DiscordRestConfig.MaxUsersPerBatch - 1) / DiscordRestConfig.MaxUsersPerBatch); + result = new List((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); else result = new List(); while (true) { - int runLimit = (limit >= DiscordRestConfig.MaxUsersPerBatch) ? DiscordRestConfig.MaxUsersPerBatch : limit; + int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; string endpoint = $"guilds/{guildId}/members?limit={runLimit}&after={afterUserId}"; var models = await SendAsync("GET", endpoint, options: options).ConfigureAwait(false); @@ -609,11 +610,11 @@ namespace Discord.API result.Add(models); - limit -= DiscordRestConfig.MaxUsersPerBatch; + limit -= DiscordConfig.MaxUsersPerBatch; afterUserId = models[models.Length - 1].User.Id; //Was this an incomplete (the last) batch? - if (models.Length != DiscordRestConfig.MaxUsersPerBatch) break; + if (models.Length != DiscordConfig.MaxUsersPerBatch) break; } if (result.Count > 1) @@ -723,14 +724,14 @@ namespace Discord.API break; } - int runs = (limit + DiscordRestConfig.MaxMessagesPerBatch - 1) / DiscordRestConfig.MaxMessagesPerBatch; - int lastRunCount = limit - (runs - 1) * DiscordRestConfig.MaxMessagesPerBatch; + int runs = (limit + DiscordConfig.MaxMessagesPerBatch - 1) / DiscordConfig.MaxMessagesPerBatch; + int lastRunCount = limit - (runs - 1) * DiscordConfig.MaxMessagesPerBatch; var result = new API.Message[runs][]; int i = 0; for (; i < runs; i++) { - int runCount = i == (runs - 1) ? lastRunCount : DiscordRestConfig.MaxMessagesPerBatch; + int runCount = i == (runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; string endpoint; if (relativeId != null) endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; @@ -769,7 +770,7 @@ namespace Discord.API } //Was this an incomplete (the last) batch? - if (models.Length != DiscordRestConfig.MaxMessagesPerBatch) { i++; break; } + if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } } if (i > 1) @@ -814,8 +815,8 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); - if (args._content.Length > DiscordRestConfig.MaxMessageSize) - throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content)); + if (args._content.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); if (guildId != 0) return await SendAsync("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); @@ -843,8 +844,8 @@ namespace Discord.API { if (args._content.Value == null) args._content = ""; - if (args._content.Value?.Length > DiscordRestConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content)); + if (args._content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } if (guildId != 0) @@ -924,8 +925,8 @@ namespace Discord.API if (args._content.IsSpecified) { Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); - if (args._content.Value.Length > DiscordRestConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordRestConfig.MaxMessageSize}.", nameof(args.Content)); + if (args._content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } if (guildId != 0) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 409994ecf..27c8a68c0 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -2,6 +2,7 @@ using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; +using Discord.Rpc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; diff --git a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs index 8843c9e73..d9a800c8e 100644 --- a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs +++ b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs @@ -1,8 +1,10 @@ -namespace Discord.API.Rest +using Discord.Rest; + +namespace Discord.API.Rest { public class GetChannelMessagesParams { - public int Limit { internal get; set; } = DiscordRestConfig.MaxMessagesPerBatch; + public int Limit { internal get; set; } = DiscordConfig.MaxMessagesPerBatch; public Direction RelativeDirection { internal get; set; } = Direction.Before; diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index e4a3a1e51..a1b2bea24 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -1,6 +1,7 @@ using Discord.API.Voice; using Discord.Logging; using Discord.Net.Converters; +using Discord.WebSocket; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; diff --git a/src/Discord.Net/Data/DataStore.cs b/src/Discord.Net/DataStore.cs similarity index 99% rename from src/Discord.Net/Data/DataStore.cs rename to src/Discord.Net/DataStore.cs index d7cfe0913..b65bf07f5 100644 --- a/src/Discord.Net/Data/DataStore.cs +++ b/src/Discord.Net/DataStore.cs @@ -1,5 +1,4 @@ -using Discord.Extensions; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 88cc96db2..698f7e3c0 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -14,6 +14,10 @@ namespace Discord public const string CDNUrl = "https://cdn.discordapp.com/"; public const string InviteUrl = "https://discord.gg/"; + public const int MaxMessageSize = 2000; + public const int MaxMessagesPerBatch = 100; + public const int MaxUsersPerBatch = 1000; + /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; } diff --git a/src/Discord.Net/Entities/Application.cs b/src/Discord.Net/Entities/Application.cs index 8b3e64b74..98545679b 100644 --- a/src/Discord.Net/Entities/Application.cs +++ b/src/Discord.Net/Entities/Application.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using System.Threading.Tasks; using Model = Discord.API.Application; diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index dee579432..0ec4b5c36 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Entities/Channels/GroupChannel.cs index 4e0a9024d..745245bb3 100644 --- a/src/Discord.Net/Entities/Channels/GroupChannel.cs +++ b/src/Discord.Net/Entities/Channels/GroupChannel.cs @@ -1,5 +1,5 @@ using Discord.API.Rest; -using Discord.Extensions; +using Discord.Rest; using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 31303bc10..a8d12d43e 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs index 7aafb6ac1..2d1713a06 100644 --- a/src/Discord.Net/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -20,9 +20,9 @@ namespace Discord /// Gets the message from this channel's cache with the given id, or null if not found. IMessage GetCachedMessage(ulong id); /// Gets the last N messages from this message channel. - Task> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch); + Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); /// Gets a collection of messages in this channel. - Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch); + Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); /// Bulk deletes multiple messages. Task DeleteMessagesAsync(IEnumerable messages); diff --git a/src/Discord.Net/Entities/Entity.cs b/src/Discord.Net/Entities/Entity.cs index 8e8e7587f..6652fd208 100644 --- a/src/Discord.Net/Entities/Entity.cs +++ b/src/Discord.Net/Entities/Entity.cs @@ -1,4 +1,6 @@ -namespace Discord +using Discord.Rest; + +namespace Discord { internal abstract class Entity : IEntity { diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index dc5b64592..5a243c5ed 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -1,6 +1,6 @@ using Discord.API.Rest; using Discord.Audio; -using Discord.Extensions; +using Discord.Rest; using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index fa2ceddda..eae31eabd 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; diff --git a/src/Discord.Net/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Entities/Guilds/UserGuild.cs index 77c28192d..0803bdb33 100644 --- a/src/Discord.Net/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/UserGuild.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Discord.Rest; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.UserGuild; diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs index 3ed09410f..d95ee1daa 100644 --- a/src/Discord.Net/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Discord.Rest; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Invite; @@ -16,8 +17,8 @@ namespace Discord public override DiscordRestClient Discord { get; } public string Code => Id; - public string Url => $"{DiscordRestConfig.InviteUrl}/{XkcdCode ?? Code}"; - public string XkcdUrl => XkcdCode != null ? $"{DiscordRestConfig.InviteUrl}/{XkcdCode}" : null; + public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; public Invite(DiscordRestClient discord, Model model) : base(model.Code) diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index df7d965af..8cd3051f2 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using Model = Discord.API.InviteMetadata; namespace Discord diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index 1dbf0c6a1..a4ff34733 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs index 43af085b3..cbcceb0cf 100644 --- a/src/Discord.Net/Entities/Roles/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -1,9 +1,7 @@ using Discord.API.Rest; +using Discord.Rest; using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Role; diff --git a/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs b/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs index ae4152cd2..bc2f441e4 100644 --- a/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs +++ b/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using Model = Discord.API.Rpc.RpcUserGuild; namespace Discord.Entities.Rpc diff --git a/src/Discord.Net/Entities/Users/GroupUser.cs b/src/Discord.Net/Entities/Users/GroupUser.cs index 7a44e5d8e..0d4bc444a 100644 --- a/src/Discord.Net/Entities/Users/GroupUser.cs +++ b/src/Discord.Net/Entities/Users/GroupUser.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Threading.Tasks; diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index caccb4336..5e6d9174e 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 32576b3e0..838a45782 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.User; diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 640084f7b..8f6ac8259 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using System.Diagnostics; using Model = Discord.API.User; diff --git a/src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs index e97c76524..de6d0544a 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs @@ -1,4 +1,5 @@ -using Discord.Extensions; +using Discord.Rest; +using Discord.WebSocket; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -51,7 +52,7 @@ namespace Discord return result; return null; } - public override IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) + public override IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (limit == 0) return ImmutableArray.Empty; diff --git a/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs b/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs index 2d47e3190..f44766b99 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs @@ -1,4 +1,6 @@ using Discord.API.Rest; +using Discord.Rest; +using Discord.WebSocket; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -25,7 +27,7 @@ namespace Discord public virtual SocketMessage Remove(ulong id) => null; public virtual SocketMessage Get(ulong id) => null; - public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) + public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => ImmutableArray.Create(); public virtual async Task DownloadAsync(ulong id) diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs b/src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs index bfc0c1c1e..92e8661eb 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Discord.WebSocket; +using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; using MessageModel = Discord.API.Message; diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs b/src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs index f0f15d0c4..368a19bfb 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs @@ -1,4 +1,4 @@ -using Discord.Extensions; +using Discord.WebSocket; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs b/src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs index 0c7d08e43..0cbec16cd 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using Discord.Rest; +using Discord.WebSocket; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -48,11 +50,11 @@ namespace Discord { return await _messages.DownloadAsync(id).ConfigureAwait(false); } - public override async Task> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch) + public override async Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) { return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); } - public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) + public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs index f017c1396..b13cc04cc 100644 --- a/src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs @@ -1,4 +1,5 @@ using Discord.Audio; +using Discord.WebSocket; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs b/src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs index befbab3aa..03fff4d98 100644 --- a/src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs @@ -1,5 +1,5 @@ using Discord.Audio; -using Discord.Extensions; +using Discord.WebSocket; using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs b/src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs index 71d73a525..8441e9143 100644 --- a/src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs +++ b/src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs @@ -1,4 +1,5 @@ -using Model = Discord.API.Message; +using Discord.WebSocket; +using Model = Discord.API.Message; namespace Discord { diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs index e56dd53ee..da1cc009f 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs @@ -1,4 +1,5 @@ -using System; +using Discord.WebSocket; +using System; using System.Diagnostics; using PresenceModel = Discord.API.Presence; diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs index 96bbdd556..bc7519748 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs @@ -1,4 +1,5 @@ -using System; +using Discord.WebSocket; +using System; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs index 0de43c9b9..a4bda52e3 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Discord.WebSocket; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs index a2fe29d46..3bae4f069 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs @@ -1,4 +1,5 @@ -using Model = Discord.API.GuildMember; +using Discord.WebSocket; +using Model = Discord.API.GuildMember; using PresenceModel = Discord.API.Presence; namespace Discord diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs b/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs index 4aceb62ab..566f626ec 100644 --- a/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs +++ b/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.WebSocket; using System; using System.Threading.Tasks; using Model = Discord.API.User; diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index 91c9f030f..8eebac817 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace Discord.Extensions +namespace Discord { internal static class CollectionExtensions { diff --git a/src/Discord.Net/Extensions/DiscordClientExtensions.cs b/src/Discord.Net/Extensions/DiscordClientExtensions.cs index dcde721d5..503d0b824 100644 --- a/src/Discord.Net/Extensions/DiscordClientExtensions.cs +++ b/src/Discord.Net/Extensions/DiscordClientExtensions.cs @@ -1,7 +1,8 @@ -using System.Linq; +using Discord.Rest; +using System.Linq; using System.Threading.Tasks; -namespace Discord.Extensions +namespace Discord { public static class DiscordClientExtensions { diff --git a/src/Discord.Net/Extensions/GuildExtensions.cs b/src/Discord.Net/Extensions/GuildExtensions.cs index e8895f22c..8446a7c01 100644 --- a/src/Discord.Net/Extensions/GuildExtensions.cs +++ b/src/Discord.Net/Extensions/GuildExtensions.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Discord.Extensions +namespace Discord { public static class GuildExtensions { diff --git a/src/Discord.Net/Extensions/GuildUserExtensions.cs b/src/Discord.Net/Extensions/GuildUserExtensions.cs index 357a4955f..492b5b76b 100644 --- a/src/Discord.Net/Extensions/GuildUserExtensions.cs +++ b/src/Discord.Net/Extensions/GuildUserExtensions.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Discord.Extensions +namespace Discord { public static class GuildUserExtensions { diff --git a/src/Discord.Net/DiscordRestClient.cs b/src/Discord.Net/Rest/DiscordRestClient.cs similarity index 99% rename from src/Discord.Net/DiscordRestClient.cs rename to src/Discord.Net/Rest/DiscordRestClient.cs index 92c27e3da..806db3eb7 100644 --- a/src/Discord.Net/DiscordRestClient.cs +++ b/src/Discord.Net/Rest/DiscordRestClient.cs @@ -10,8 +10,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Runtime.InteropServices; +using Discord.Rpc; +using Discord.WebSocket; -namespace Discord +namespace Discord.Rest { public class DiscordRestClient : IDiscordClient { diff --git a/src/Discord.Net/DiscordRestConfig.cs b/src/Discord.Net/Rest/DiscordRestConfig.cs similarity index 77% rename from src/Discord.Net/DiscordRestConfig.cs rename to src/Discord.Net/Rest/DiscordRestConfig.cs index 16252c854..8dee72231 100644 --- a/src/Discord.Net/DiscordRestConfig.cs +++ b/src/Discord.Net/Rest/DiscordRestConfig.cs @@ -1,15 +1,11 @@ using Discord.Net.Rest; -namespace Discord +namespace Discord.Rest { public class DiscordRestConfig : DiscordConfig { public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - - public const int MaxMessageSize = 2000; - public const int MaxMessagesPerBatch = 100; - public const int MaxUsersPerBatch = 1000; - + internal const int RestTimeout = 10000; internal const int MessageQueueInterval = 100; internal const int WebSocketQueueInterval = 100; diff --git a/src/Discord.Net/DiscordRpcClient.cs b/src/Discord.Net/Rpc/DiscordRpcClient.cs similarity index 93% rename from src/Discord.Net/DiscordRpcClient.cs rename to src/Discord.Net/Rpc/DiscordRpcClient.cs index 453ca4ac3..c798f1025 100644 --- a/src/Discord.Net/DiscordRpcClient.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.cs @@ -2,6 +2,7 @@ using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Queue; +using Discord.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -9,18 +10,10 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -namespace Discord +namespace Discord.Rpc { - public class DiscordRpcClient : DiscordRestClient + public partial class DiscordRpcClient : DiscordRestClient { - public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - - public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } - private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); - private readonly ILogger _rpcLogger; private readonly JsonSerializer _serializer; diff --git a/src/Discord.Net/DiscordRpcConfig.cs b/src/Discord.Net/Rpc/DiscordRpcConfig.cs similarity index 95% rename from src/Discord.Net/DiscordRpcConfig.cs rename to src/Discord.Net/Rpc/DiscordRpcConfig.cs index c7e38bd44..32c63fc01 100644 --- a/src/Discord.Net/DiscordRpcConfig.cs +++ b/src/Discord.Net/Rpc/DiscordRpcConfig.cs @@ -1,6 +1,7 @@ using Discord.Net.WebSockets; +using Discord.Rest; -namespace Discord +namespace Discord.Rpc { public class DiscordRpcConfig : DiscordRestConfig { diff --git a/src/Discord.Net/Rpc/RpcEvents.cs b/src/Discord.Net/Rpc/RpcEvents.cs new file mode 100644 index 000000000..6d2e85e8f --- /dev/null +++ b/src/Discord.Net/Rpc/RpcEvents.cs @@ -0,0 +1,6 @@ +namespace Discord.Rpc +{ + public enum RpcEvent + { + } +} diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs similarity index 99% rename from src/Discord.Net/DiscordSocketClient.Events.cs rename to src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs index 7182a7070..04aadaa3f 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/WebSocket/DiscordSocketClient.Events.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace Discord +namespace Discord.WebSocket { //TODO: Add event docstrings public partial class DiscordSocketClient diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/WebSocket/DiscordSocketClient.cs similarity index 99% rename from src/Discord.Net/DiscordSocketClient.cs rename to src/Discord.Net/WebSocket/DiscordSocketClient.cs index 72d48079f..46c0c6a2e 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/WebSocket/DiscordSocketClient.cs @@ -1,10 +1,10 @@ using Discord.API.Gateway; using Discord.Audio; -using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Queue; using Discord.Net.WebSockets; +using Discord.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -15,7 +15,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace Discord +namespace Discord.WebSocket { public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient { diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs similarity index 98% rename from src/Discord.Net/DiscordSocketConfig.cs rename to src/Discord.Net/WebSocket/DiscordSocketConfig.cs index cbb63e514..b26fcb0da 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs @@ -1,5 +1,6 @@ using Discord.Audio; using Discord.Net.WebSockets; +using Discord.Rest; namespace Discord { From 934d080be5ffc5057593834a71bda74951ca8575 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 17:20:14 -0300 Subject: [PATCH 19/24] Started adding Rpc events --- src/Discord.Net/Rpc/DiscordRpcClient.Events.cs | 64 ++++++++++++++++++++++++++ src/Discord.Net/Rpc/DiscordRpcClient.cs | 56 +++++++++++++++++++++- 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net/Rpc/DiscordRpcClient.Events.cs diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs b/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs new file mode 100644 index 000000000..f614642af --- /dev/null +++ b/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Rpc +{ + public partial class DiscordRpcClient + { + //General + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + + //Guild + public event Func GuildUpdated + { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Voice + public event Func VoiceStateUpdated + { + add { _voiceStateUpdatedEvent.Add(value); } + remove { _voiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _voiceStateUpdatedEvent = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); + public event Func MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.cs b/src/Discord.Net/Rpc/DiscordRpcClient.cs index c798f1025..3916f4696 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.cs @@ -237,7 +237,7 @@ namespace Discord.Rpc { //CancellationToken = cancelToken //TODO: Implement }; - + if (LoginState != LoginState.LoggedOut) await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); //Has bearer @@ -253,6 +253,58 @@ namespace Discord.Rpc } break; + //Guilds + case "GUILD_STATUS": + { + await _guildUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + + //Voice + case "VOICE_STATE_CREATE": + { + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + case "VOICE_STATE_UPDATE": + { + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + case "VOICE_STATE_DELETE": + { + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + + case "SPEAKING_START": + { + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + case "SPEAKING_STOP": + { + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + + //Messages + case "MESSAGE_CREATE": + { + await _messageReceivedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + case "MESSAGE_UPDATE": + { + await _messageUpdatedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + case "MESSAGE_DELETE": + { + await _messageDeletedEvent.InvokeAsync().ConfigureAwait(false); + } + break; + //Others default: await _rpcLogger.WarningAsync($"Unknown Dispatch ({evnt})").ConfigureAwait(false); @@ -260,7 +312,7 @@ namespace Discord.Rpc } break; - /*default: + /*default: //Other opcodes are used for command responses await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); return;*/ } From ab9e70dc677156f74111be3def4652ba65370e6e Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 17:54:38 -0300 Subject: [PATCH 20/24] Added RPC Subscription funcs and Message Events --- src/Discord.Net/API/Rpc/MessageEvent.cs | 10 ++--- src/Discord.Net/Entities/Messages/Message.cs | 11 ++--- src/Discord.Net/Rpc/DiscordRpcClient.Events.cs | 12 ++--- src/Discord.Net/Rpc/DiscordRpcClient.cs | 62 ++++++++++++++++++++++++-- src/Discord.Net/Rpc/RpcChannelEvent.cs | 14 ++++++ src/Discord.Net/Rpc/RpcEvents.cs | 6 --- src/Discord.Net/Rpc/RpcGuildEvent.cs | 7 +++ 7 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 src/Discord.Net/Rpc/RpcChannelEvent.cs delete mode 100644 src/Discord.Net/Rpc/RpcEvents.cs create mode 100644 src/Discord.Net/Rpc/RpcGuildEvent.cs diff --git a/src/Discord.Net/API/Rpc/MessageEvent.cs b/src/Discord.Net/API/Rpc/MessageEvent.cs index 4aaa87f33..337fec910 100644 --- a/src/Discord.Net/API/Rpc/MessageEvent.cs +++ b/src/Discord.Net/API/Rpc/MessageEvent.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - +using Newtonsoft.Json; namespace Discord.API.Rpc { public class MessageEvent { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("message")] + public Message Message { get; set; } } } diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index a4ff34733..aaf81aad2 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -40,13 +40,10 @@ namespace Discord Channel = channel; Author = author; Type = model.Type; - - if (channel is IGuildChannel) - { - MentionedUsers = ImmutableArray.Create(); - MentionedChannelIds = ImmutableArray.Create(); - MentionedRoles = ImmutableArray.Create(); - } + + MentionedUsers = ImmutableArray.Create(); + MentionedChannelIds = ImmutableArray.Create(); + MentionedRoles = ImmutableArray.Create(); Update(model, UpdateSource.Creation); } diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs b/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs index f614642af..25989d01a 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.Events.cs @@ -42,23 +42,23 @@ namespace Discord.Rpc private readonly AsyncEvent> _voiceStateUpdatedEvent = new AsyncEvent>(); //Messages - public event Func MessageReceived + public event Func MessageReceived { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func MessageUpdated + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func MessageUpdated { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); - public event Func MessageDeleted + private readonly AsyncEvent> _messageUpdatedEvent = new AsyncEvent>(); + public event Func MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _messageDeletedEvent = new AsyncEvent>(); } } diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.cs b/src/Discord.Net/Rpc/DiscordRpcClient.cs index 3916f4696..c4bb1c38b 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.cs @@ -213,6 +213,57 @@ namespace Discord.Rpc return result.Code; } + public async Task SubscribeGuild(ulong guildId, params RpcChannelEvent[] events) + { + Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); + for (int i = 0; i < events.Length; i++) + await ApiClient.SendGuildSubscribeAsync(GetEventName(events[i]), guildId); + } + public async Task UnsubscribeGuild(ulong guildId, params RpcChannelEvent[] events) + { + Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); + for (int i = 0; i < events.Length; i++) + await ApiClient.SendGuildUnsubscribeAsync(GetEventName(events[i]), guildId); + } + public async Task SubscribeChannel(ulong channelId, params RpcChannelEvent[] events) + { + Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); + for (int i = 0; i < events.Length; i++) + await ApiClient.SendChannelSubscribeAsync(GetEventName(events[i]), channelId); + } + public async Task UnsubscribeChannel(ulong channelId, params RpcChannelEvent[] events) + { + Preconditions.AtLeast(events?.Length ?? 0, 1, nameof(events)); + for (int i = 0; i < events.Length; i++) + await ApiClient.SendChannelUnsubscribeAsync(GetEventName(events[i]), channelId); + } + + private static string GetEventName(RpcGuildEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcGuildEvent.GuildStatus: return "GUILD_STATUS"; + default: + throw new InvalidOperationException($"Unknown RPC Guild Event: {rpcEvent}"); + } + } + private static string GetEventName(RpcChannelEvent rpcEvent) + { + switch (rpcEvent) + { + case RpcChannelEvent.VoiceStateCreate: return "VOICE_STATE_CREATE"; + case RpcChannelEvent.VoiceStateUpdate: return "VOICE_STATE_UPDATE"; + case RpcChannelEvent.VoiceStateDelete: return "VOICE_STATE_DELETE"; + case RpcChannelEvent.SpeakingStart: return "SPEAKING_START"; + case RpcChannelEvent.SpeakingStop: return "SPEAKING_STOP"; + case RpcChannelEvent.MessageCreate: return "MESSAGE_CREATE"; + case RpcChannelEvent.MessageUpdate: return "MESSAGE_UPDATE"; + case RpcChannelEvent.MessageDelete: return "MESSAGE_DELETE"; + default: + throw new InvalidOperationException($"Unknown RPC Channel Event: {rpcEvent}"); + } + } + private async Task ProcessMessageAsync(string cmd, Optional evnt, Optional payload) { try @@ -291,17 +342,22 @@ namespace Discord.Rpc //Messages case "MESSAGE_CREATE": { - await _messageReceivedEvent.InvokeAsync().ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } break; case "MESSAGE_UPDATE": { - await _messageUpdatedEvent.InvokeAsync().ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } break; case "MESSAGE_DELETE": { - await _messageDeletedEvent.InvokeAsync().ConfigureAwait(false); + var data = (payload.Value as JToken).ToObject(_serializer); + await _messageDeletedEvent.InvokeAsync(data.ChannelId, data.Message.Id).ConfigureAwait(false); } break; diff --git a/src/Discord.Net/Rpc/RpcChannelEvent.cs b/src/Discord.Net/Rpc/RpcChannelEvent.cs new file mode 100644 index 000000000..2916dc88d --- /dev/null +++ b/src/Discord.Net/Rpc/RpcChannelEvent.cs @@ -0,0 +1,14 @@ +namespace Discord.Rpc +{ + public enum RpcChannelEvent + { + VoiceStateCreate, + VoiceStateUpdate, + VoiceStateDelete, + SpeakingStart, + SpeakingStop, + MessageCreate, + MessageUpdate, + MessageDelete + } +} diff --git a/src/Discord.Net/Rpc/RpcEvents.cs b/src/Discord.Net/Rpc/RpcEvents.cs deleted file mode 100644 index 6d2e85e8f..000000000 --- a/src/Discord.Net/Rpc/RpcEvents.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord.Rpc -{ - public enum RpcEvent - { - } -} diff --git a/src/Discord.Net/Rpc/RpcGuildEvent.cs b/src/Discord.Net/Rpc/RpcGuildEvent.cs new file mode 100644 index 000000000..34e5dc390 --- /dev/null +++ b/src/Discord.Net/Rpc/RpcGuildEvent.cs @@ -0,0 +1,7 @@ +namespace Discord.Rpc +{ + public enum RpcGuildEvent + { + GuildStatus + } +} From 6d2a30a7395c79a37e6f9f65a94406d90914b40c Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 18:00:16 -0300 Subject: [PATCH 21/24] Fixed typo --- src/Discord.Net/WebSocket/DiscordSocketClient.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/WebSocket/DiscordSocketClient.cs b/src/Discord.Net/WebSocket/DiscordSocketClient.cs index 46c0c6a2e..e78663c1e 100644 --- a/src/Discord.Net/WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net/WebSocket/DiscordSocketClient.cs @@ -803,11 +803,6 @@ namespace Discord.WebSocket } } break; - case "CHANNEL_PINS_UPDATE": - { - await _gatewayLogger.DebugAsync("Ignored Disbatch (CHANNEL_PINS_UPDATE)"); - } - break; case "CHANNEL_DELETE": { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); @@ -1476,6 +1471,9 @@ namespace Discord.WebSocket return; //Ignored (User only) + case "CHANNEL_PINS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)"); + break; case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); return; From 79f11455b92a9443f24d587065dd439186775191 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 18:02:39 -0300 Subject: [PATCH 22/24] Added logging for RPC events --- src/Discord.Net/Rpc/DiscordRpcClient.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.cs b/src/Discord.Net/Rpc/DiscordRpcClient.cs index c4bb1c38b..7cdbccfff 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.cs @@ -307,6 +307,8 @@ namespace Discord.Rpc //Guilds case "GUILD_STATUS": { + await _rpcLogger.DebugAsync("Received Dispatch (GUILD_STATUS)").ConfigureAwait(false); + await _guildUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; @@ -314,27 +316,36 @@ namespace Discord.Rpc //Voice case "VOICE_STATE_CREATE": { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_CREATE)").ConfigureAwait(false); + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; case "VOICE_STATE_UPDATE": { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; case "VOICE_STATE_DELETE": { + await _rpcLogger.DebugAsync("Received Dispatch (VOICE_STATE_DELETE)").ConfigureAwait(false); + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; case "SPEAKING_START": { + await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_START)").ConfigureAwait(false); await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; case "SPEAKING_STOP": { + await _rpcLogger.DebugAsync("Received Dispatch (SPEAKING_STOP)").ConfigureAwait(false); + await _voiceStateUpdatedEvent.InvokeAsync().ConfigureAwait(false); } break; @@ -342,21 +353,27 @@ namespace Discord.Rpc //Messages case "MESSAGE_CREATE": { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } break; case "MESSAGE_UPDATE": { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } break; case "MESSAGE_DELETE": { + await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); + await _messageDeletedEvent.InvokeAsync(data.ChannelId, data.Message.Id).ConfigureAwait(false); } break; From 260a0153f450688f3b66b90cde605777bc333fab Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 18:11:41 -0300 Subject: [PATCH 23/24] Moved Rpc/WebSocket entities, fixed nullref on RPC message events --- src/Discord.Net/API/DiscordRpcAPIClient.cs | 8 ++++---- src/Discord.Net/Entities/Messages/Message.cs | 1 - src/Discord.Net/Rpc/DiscordRpcClient.cs | 4 ++-- .../{Entities/Rpc => Rpc/Entities}/IRemoteUserGuild.cs | 6 +++--- .../{Entities/Rpc => Rpc/Entities}/RemoteUserGuild.cs | 6 +++--- src/Discord.Net/Rpc/Entities/RpcMessage.cs | 15 +++++++++++++++ .../Entities}/Channels/ISocketChannel.cs | 0 .../Entities}/Channels/ISocketGuildChannel.cs | 0 .../Entities}/Channels/ISocketMessageChannel.cs | 0 .../Entities}/Channels/ISocketPrivateChannel.cs | 0 .../Entities}/Channels/MessageCache.cs | 0 .../Entities}/Channels/MessageManager.cs | 0 .../Entities}/Channels/SocketDMChannel.cs | 0 .../Entities}/Channels/SocketGroupChannel.cs | 0 .../Entities}/Channels/SocketTextChannel.cs | 0 .../Entities}/Channels/SocketVoiceChannel.cs | 0 .../Entities}/Guilds/SocketGuild.cs | 0 .../Entities}/Messages/SocketMessage.cs | 0 .../WebSocket => WebSocket/Entities}/Users/ISocketUser.cs | 0 .../WebSocket => WebSocket/Entities}/Users/Presence.cs | 0 .../Entities}/Users/SocketDMUser.cs | 0 .../Entities}/Users/SocketGlobalUser.cs | 0 .../Entities}/Users/SocketGroupUser.cs | 0 .../Entities}/Users/SocketGuildUser.cs | 0 .../Entities}/Users/SocketSelfUser.cs | 0 .../WebSocket => WebSocket/Entities}/Users/VoiceState.cs | 0 26 files changed, 27 insertions(+), 13 deletions(-) rename src/Discord.Net/{Entities/Rpc => Rpc/Entities}/IRemoteUserGuild.cs (51%) rename src/Discord.Net/{Entities/Rpc => Rpc/Entities}/RemoteUserGuild.cs (86%) create mode 100644 src/Discord.Net/Rpc/Entities/RpcMessage.cs rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/ISocketChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/ISocketGuildChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/ISocketMessageChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/ISocketPrivateChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/MessageCache.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/MessageManager.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/SocketDMChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/SocketGroupChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/SocketTextChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Channels/SocketVoiceChannel.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Guilds/SocketGuild.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Messages/SocketMessage.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/ISocketUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/Presence.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/SocketDMUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/SocketGlobalUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/SocketGroupUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/SocketGuildUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/SocketSelfUser.cs (100%) rename src/Discord.Net/{Entities/WebSocket => WebSocket/Entities}/Users/VoiceState.cs (100%) diff --git a/src/Discord.Net/API/DiscordRpcAPIClient.cs b/src/Discord.Net/API/DiscordRpcAPIClient.cs index 27c8a68c0..e82c5c83f 100644 --- a/src/Discord.Net/API/DiscordRpcAPIClient.cs +++ b/src/Discord.Net/API/DiscordRpcAPIClient.cs @@ -90,7 +90,7 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) ProcessMessage(msg); @@ -102,7 +102,7 @@ namespace Discord.API using (var reader = new StringReader(text)) using (var jsonReader = new JsonTextReader(reader)) { - var msg = _serializer.Deserialize(jsonReader); + var msg = _serializer.Deserialize(jsonReader); await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data).ConfigureAwait(false); if (msg.Nonce.IsSpecified && msg.Nonce.Value.HasValue) ProcessMessage(msg); @@ -219,7 +219,7 @@ namespace Discord.API { byte[] bytes = null; var guid = Guid.NewGuid(); - payload = new RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; + payload = new API.Rpc.RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; if (payload != null) { var json = SerializeJson(payload); @@ -338,7 +338,7 @@ namespace Discord.API return await SendRpcAsync("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); } - private bool ProcessMessage(RpcMessage msg) + private bool ProcessMessage(API.Rpc.RpcMessage msg) { RpcRequest requestTracker; if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index aaf81aad2..df6bf7e2c 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -53,7 +53,6 @@ namespace Discord var guildChannel = Channel as GuildChannel; var guild = guildChannel?.Guild; - var discord = Discord; if (model.IsTextToSpeech.IsSpecified) IsTTS = model.IsTextToSpeech.Value; diff --git a/src/Discord.Net/Rpc/DiscordRpcClient.cs b/src/Discord.Net/Rpc/DiscordRpcClient.cs index 7cdbccfff..53886d41d 100644 --- a/src/Discord.Net/Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net/Rpc/DiscordRpcClient.cs @@ -355,7 +355,7 @@ namespace Discord.Rpc { await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); - var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + var msg = new RpcMessage(this, data.Message); await _messageReceivedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } @@ -364,7 +364,7 @@ namespace Discord.Rpc { await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload.Value as JToken).ToObject(_serializer); - var msg = new Message(null, new User(data.Message.Author.Value), data.Message); + var msg = new RpcMessage(this, data.Message); await _messageUpdatedEvent.InvokeAsync(data.ChannelId, msg).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs b/src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs similarity index 51% rename from src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs rename to src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs index f1cdc8203..5f92edb9b 100644 --- a/src/Discord.Net/Entities/Rpc/IRemoteUserGuild.cs +++ b/src/Discord.Net/Rpc/Entities/IRemoteUserGuild.cs @@ -1,8 +1,8 @@ -namespace Discord.Entities.Rpc +namespace Discord.Rpc { - public interface IRemoteUserGuild : ISnowflakeEntity + /*public interface IRemoteUserGuild : ISnowflakeEntity { /// Gets the name of this guild. string Name { get; } - } + }*/ } diff --git a/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs b/src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs similarity index 86% rename from src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs rename to src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs index bc2f441e4..e0337237a 100644 --- a/src/Discord.Net/Entities/Rpc/RemoteUserGuild.cs +++ b/src/Discord.Net/Rpc/Entities/RemoteUserGuild.cs @@ -2,9 +2,9 @@ using System; using Model = Discord.API.Rpc.RpcUserGuild; -namespace Discord.Entities.Rpc +namespace Discord.Rpc { - internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity + /*internal class RemoteUserGuild : IRemoteUserGuild, ISnowflakeEntity { public ulong Id { get; } public DiscordRestClient Discord { get; } @@ -26,5 +26,5 @@ namespace Discord.Entities.Rpc } bool IEntity.IsAttached => false; - } + }*/ } diff --git a/src/Discord.Net/Rpc/Entities/RpcMessage.cs b/src/Discord.Net/Rpc/Entities/RpcMessage.cs new file mode 100644 index 000000000..86d67bfa4 --- /dev/null +++ b/src/Discord.Net/Rpc/Entities/RpcMessage.cs @@ -0,0 +1,15 @@ +using Discord.Rest; + +namespace Discord.Rpc +{ + internal class RpcMessage : Message + { + public override DiscordRestClient Discord { get; } + + public RpcMessage(DiscordRpcClient discord, API.Message model) + : base(null, model.Author.IsSpecified ? new User(model.Author.Value) : null, model) + { + Discord = discord; + } + } +} diff --git a/src/Discord.Net/Entities/WebSocket/Channels/ISocketChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/ISocketChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/ISocketChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/ISocketGuildChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/ISocketGuildChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/ISocketGuildChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/ISocketMessageChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/ISocketMessageChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/ISocketMessageChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/ISocketPrivateChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/ISocketPrivateChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/ISocketPrivateChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs b/src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/MessageCache.cs rename to src/Discord.Net/WebSocket/Entities/Channels/MessageCache.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs b/src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/MessageManager.cs rename to src/Discord.Net/WebSocket/Entities/Channels/MessageManager.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/SocketDMChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/SocketDMChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/SocketGroupChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/SocketGroupChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/SocketTextChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/SocketTextChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Channels/SocketVoiceChannel.cs rename to src/Discord.Net/WebSocket/Entities/Channels/SocketVoiceChannel.cs diff --git a/src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs b/src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Guilds/SocketGuild.cs rename to src/Discord.Net/WebSocket/Entities/Guilds/SocketGuild.cs diff --git a/src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs b/src/Discord.Net/WebSocket/Entities/Messages/SocketMessage.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Messages/SocketMessage.cs rename to src/Discord.Net/WebSocket/Entities/Messages/SocketMessage.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/ISocketUser.cs b/src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/ISocketUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/ISocketUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/Presence.cs b/src/Discord.Net/WebSocket/Entities/Users/Presence.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/Presence.cs rename to src/Discord.Net/WebSocket/Entities/Users/Presence.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/SocketDMUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/SocketDMUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGlobalUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/SocketGlobalUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/SocketGlobalUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGroupUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/SocketGroupUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/SocketGroupUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketGuildUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/SocketGuildUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/SocketGuildUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SocketSelfUser.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/SocketSelfUser.cs rename to src/Discord.Net/WebSocket/Entities/Users/SocketSelfUser.cs diff --git a/src/Discord.Net/Entities/WebSocket/Users/VoiceState.cs b/src/Discord.Net/WebSocket/Entities/Users/VoiceState.cs similarity index 100% rename from src/Discord.Net/Entities/WebSocket/Users/VoiceState.cs rename to src/Discord.Net/WebSocket/Entities/Users/VoiceState.cs From 7a36da2b58f36e32a7db3d9f1545e1ad2b44068f Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 29 Jul 2016 18:16:57 -0300 Subject: [PATCH 24/24] Moved REST internal classes --- src/Discord.Net/{ => Rest}/Entities/Application.cs | 0 src/Discord.Net/{ => Rest}/Entities/Channels/DMChannel.cs | 0 src/Discord.Net/{ => Rest}/Entities/Channels/GroupChannel.cs | 0 src/Discord.Net/{ => Rest}/Entities/Channels/GuildChannel.cs | 0 src/Discord.Net/{ => Rest}/Entities/Channels/TextChannel.cs | 0 src/Discord.Net/{ => Rest}/Entities/Channels/VoiceChannel.cs | 0 src/Discord.Net/{ => Rest}/Entities/Entity.cs | 0 src/Discord.Net/{ => Rest}/Entities/Guilds/Guild.cs | 0 src/Discord.Net/{ => Rest}/Entities/Guilds/GuildIntegration.cs | 0 src/Discord.Net/{ => Rest}/Entities/Guilds/UserGuild.cs | 0 src/Discord.Net/{ => Rest}/Entities/Guilds/VoiceRegion.cs | 0 src/Discord.Net/{ => Rest}/Entities/Invites/Invite.cs | 0 src/Discord.Net/{ => Rest}/Entities/Invites/InviteMetadata.cs | 0 src/Discord.Net/{ => Rest}/Entities/Messages/Attachment.cs | 0 src/Discord.Net/{ => Rest}/Entities/Messages/Embed.cs | 0 src/Discord.Net/{ => Rest}/Entities/Messages/Message.cs | 0 src/Discord.Net/{ => Rest}/Entities/Roles/Role.cs | 0 src/Discord.Net/{ => Rest}/Entities/SnowflakeEntity.cs | 0 src/Discord.Net/{ => Rest}/Entities/Users/Connection.cs | 0 src/Discord.Net/{ => Rest}/Entities/Users/GroupUser.cs | 0 src/Discord.Net/{ => Rest}/Entities/Users/GuildUser.cs | 0 src/Discord.Net/{ => Rest}/Entities/Users/SelfUser.cs | 0 src/Discord.Net/{ => Rest}/Entities/Users/User.cs | 0 23 files changed, 0 insertions(+), 0 deletions(-) rename src/Discord.Net/{ => Rest}/Entities/Application.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Channels/DMChannel.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Channels/GroupChannel.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Channels/GuildChannel.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Channels/TextChannel.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Channels/VoiceChannel.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Entity.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Guilds/Guild.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Guilds/GuildIntegration.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Guilds/UserGuild.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Guilds/VoiceRegion.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Invites/Invite.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Invites/InviteMetadata.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Messages/Attachment.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Messages/Embed.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Messages/Message.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Roles/Role.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/SnowflakeEntity.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Users/Connection.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Users/GroupUser.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Users/GuildUser.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Users/SelfUser.cs (100%) rename src/Discord.Net/{ => Rest}/Entities/Users/User.cs (100%) diff --git a/src/Discord.Net/Entities/Application.cs b/src/Discord.Net/Rest/Entities/Application.cs similarity index 100% rename from src/Discord.Net/Entities/Application.cs rename to src/Discord.Net/Rest/Entities/Application.cs diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/DMChannel.cs rename to src/Discord.Net/Rest/Entities/Channels/DMChannel.cs diff --git a/src/Discord.Net/Entities/Channels/GroupChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/GroupChannel.cs rename to src/Discord.Net/Rest/Entities/Channels/GroupChannel.cs diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/GuildChannel.cs rename to src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/TextChannel.cs rename to src/Discord.Net/Rest/Entities/Channels/TextChannel.cs diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs similarity index 100% rename from src/Discord.Net/Entities/Channels/VoiceChannel.cs rename to src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs diff --git a/src/Discord.Net/Entities/Entity.cs b/src/Discord.Net/Rest/Entities/Entity.cs similarity index 100% rename from src/Discord.Net/Entities/Entity.cs rename to src/Discord.Net/Rest/Entities/Entity.cs diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/Guild.cs rename to src/Discord.Net/Rest/Entities/Guilds/Guild.cs diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/GuildIntegration.cs rename to src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs diff --git a/src/Discord.Net/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/UserGuild.cs rename to src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs diff --git a/src/Discord.Net/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs similarity index 100% rename from src/Discord.Net/Entities/Guilds/VoiceRegion.cs rename to src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Rest/Entities/Invites/Invite.cs similarity index 100% rename from src/Discord.Net/Entities/Invites/Invite.cs rename to src/Discord.Net/Rest/Entities/Invites/Invite.cs diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs similarity index 100% rename from src/Discord.Net/Entities/Invites/InviteMetadata.cs rename to src/Discord.Net/Rest/Entities/Invites/InviteMetadata.cs diff --git a/src/Discord.Net/Entities/Messages/Attachment.cs b/src/Discord.Net/Rest/Entities/Messages/Attachment.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/Attachment.cs rename to src/Discord.Net/Rest/Entities/Messages/Attachment.cs diff --git a/src/Discord.Net/Entities/Messages/Embed.cs b/src/Discord.Net/Rest/Entities/Messages/Embed.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/Embed.cs rename to src/Discord.Net/Rest/Entities/Messages/Embed.cs diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Rest/Entities/Messages/Message.cs similarity index 100% rename from src/Discord.Net/Entities/Messages/Message.cs rename to src/Discord.Net/Rest/Entities/Messages/Message.cs diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Rest/Entities/Roles/Role.cs similarity index 100% rename from src/Discord.Net/Entities/Roles/Role.cs rename to src/Discord.Net/Rest/Entities/Roles/Role.cs diff --git a/src/Discord.Net/Entities/SnowflakeEntity.cs b/src/Discord.Net/Rest/Entities/SnowflakeEntity.cs similarity index 100% rename from src/Discord.Net/Entities/SnowflakeEntity.cs rename to src/Discord.Net/Rest/Entities/SnowflakeEntity.cs diff --git a/src/Discord.Net/Entities/Users/Connection.cs b/src/Discord.Net/Rest/Entities/Users/Connection.cs similarity index 100% rename from src/Discord.Net/Entities/Users/Connection.cs rename to src/Discord.Net/Rest/Entities/Users/Connection.cs diff --git a/src/Discord.Net/Entities/Users/GroupUser.cs b/src/Discord.Net/Rest/Entities/Users/GroupUser.cs similarity index 100% rename from src/Discord.Net/Entities/Users/GroupUser.cs rename to src/Discord.Net/Rest/Entities/Users/GroupUser.cs diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs similarity index 100% rename from src/Discord.Net/Entities/Users/GuildUser.cs rename to src/Discord.Net/Rest/Entities/Users/GuildUser.cs diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs similarity index 100% rename from src/Discord.Net/Entities/Users/SelfUser.cs rename to src/Discord.Net/Rest/Entities/Users/SelfUser.cs diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs similarity index 100% rename from src/Discord.Net/Entities/Users/User.cs rename to src/Discord.Net/Rest/Entities/Users/User.cs