| @@ -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<TResponse> SendAsync<TResponse>(string method, string endpoint, | |||
| GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class | |||
| => DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false)); | |||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket = | |||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, GlobalBucket bucket = | |||
| GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class | |||
| => DeserializeJson<TResponse>(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<TResponse> SendAsync<TResponse>(string method, string endpoint, | |||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, | |||
| GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class | |||
| => DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, null, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); | |||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, | |||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, object payload, | |||
| GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class | |||
| => DeserializeJson<TResponse>(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<Stream> SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, | |||
| private async Task<Stream> 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<Stream> SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, bool headerOnly, | |||
| private async Task<Stream> SendMultipartInternalAsync(string method, string endpoint, IReadOnlyDictionary<string, object> 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); | |||
| @@ -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<string, Task> SentRpcMessage { add { _sentRpcMessageEvent.Add(value); } remove { _sentRpcMessageEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<string, Task>> _sentRpcMessageEvent = new AsyncEvent<Func<string, Task>>(); | |||
| public event Func<string, string, object, string, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<string, string, object, string, Task>> _receivedRpcEvent = new AsyncEvent<Func<string, string, object, string, Task>>(); | |||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| 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<RpcMessage>(reader.ReadToEnd()); | |||
| await _receivedRpcEvent.InvokeAsync(msg.Cmd, msg.Event, msg.Data, msg.Nonce).ConfigureAwait(false); | |||
| } | |||
| } | |||
| }; | |||
| _webSocketClient.TextMessage += async text => | |||
| { | |||
| var msg = JsonConvert.DeserializeObject<RpcMessage>(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<T>(Stream jsonStream) | |||
| { | |||
| using (TextReader text = new StreamReader(jsonStream)) | |||
| using (JsonReader reader = new JsonTextReader(text)) | |||
| return _serializer.Deserialize<T>(reader); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| using Newtonsoft.Json; | |||
| namespace Discord.API.Rpc | |||
| { | |||
| public class AuthenticateParams | |||
| { | |||
| [JsonProperty("access_token")] | |||
| public string AccessToken { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| namespace Discord.API.Rpc | |||
| { | |||
| public class AuthorizeEvent | |||
| { | |||
| [JsonProperty("code")] | |||
| public string Code { get; set; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| 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); | |||
| @@ -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<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
| public event Func<Task> LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _loggedInEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | |||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| public event Func<Task> Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _readyEvent = new AsyncEvent<Func<Task>>(); | |||
| private readonly ILogger _clientLogger, _rpcLogger; | |||
| private readonly SemaphoreSlim _connectionLock; | |||
| private readonly JsonSerializer _serializer; | |||
| private TaskCompletionSource<bool> _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; } | |||
| /// <summary> Creates a new RPC discord client. </summary> | |||
| public DiscordRpcClient(string clientId) : this(new DiscordRpcConfig(clientId)) { } | |||
| /// <summary> Creates a new RPC discord client. </summary> | |||
| 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); | |||
| /// <inheritdoc /> | |||
| 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); | |||
| } | |||
| /// <inheritdoc /> | |||
| 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); | |||
| } | |||
| /// <inheritdoc /> | |||
| 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<bool>(); | |||
| _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; | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| 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<ReadyEvent>(_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<AuthorizeEvent>(_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<AuthenticateEvent>(_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(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| /// <summary> Gets or sets the Discord client/application id used for this RPC connection. </summary> | |||
| public string ClientId { get; set; } | |||
| /// <summary> Gets or sets the provider used to generate new websocket connections. </summary> | |||
| public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); | |||
| } | |||
| } | |||
| @@ -14,11 +14,6 @@ namespace Discord | |||
| /// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary> | |||
| public int MessageCacheSize { get; set; } = 0; | |||
| /*/// <summary> | |||
| /// 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. | |||
| /// </summary> | |||
| public bool UsePermissionsCache { get; set; } = false;*/ | |||
| /// <summary> | |||
| /// 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. | |||
| @@ -17,7 +17,7 @@ namespace Discord | |||
| { | |||
| protected ConcurrentDictionary<ulong, GroupUser> _users; | |||
| private string _iconId; | |||
| public override DiscordRestClient Discord { get; } | |||
| public string Name { get; private set; } | |||
| @@ -12,7 +12,7 @@ namespace Discord | |||
| private readonly DiscordSocketClient _discord; | |||
| private readonly ISocketMessageChannel _channel; | |||
| public virtual IReadOnlyCollection<SocketMessage> Messages | |||
| public virtual IReadOnlyCollection<SocketMessage> Messages | |||
| => ImmutableArray.Create<SocketMessage>(); | |||
| public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) | |||
| @@ -5,7 +5,10 @@ | |||
| GeneralRest, | |||
| DirectMessage, | |||
| SendEditMessage, | |||
| GeneralGateway, | |||
| UpdateStatus | |||
| UpdateStatus, | |||
| GeneralRpc | |||
| } | |||
| } | |||
| @@ -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<GuildBucket, Bucket> | |||