From 0664442bf1896f2d048cab3618ad167cd840b2a8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 15 Jul 2016 12:58:28 -0300 Subject: [PATCH] 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 +- ...ticateEvent.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 + .../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 + .../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 + .../API/Rpc/SetLocalVolumeParams.cs | 10 + .../API/Rpc/SetLocalVolumeResponse.cs | 12 + src/Discord.Net/API/Rpc/SpeakingEvent.cs | 11 + .../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 +- .../Entities/Rpc/IRemoteUserGuild.cs | 8 + .../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; + } + } +}