| @@ -88,13 +88,8 @@ namespace Discord.Commands | |||||
| throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); | ||||
| bool isUnparsed = parameter.GetCustomAttribute<UnparsedAttribute>() != null; | bool isUnparsed = parameter.GetCustomAttribute<UnparsedAttribute>() != 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 name = parameter.Name; | ||||
| string description = typeInfo.GetCustomAttribute<DescriptionAttribute>()?.Text; | string description = typeInfo.GetCustomAttribute<DescriptionAttribute>()?.Text; | ||||
| @@ -3,14 +3,14 @@ | |||||
| internal static class CDN | internal static class CDN | ||||
| { | { | ||||
| public static string GetApplicationIconUrl(ulong appId, string iconId) | 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) | 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) | 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) | 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) | 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; | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,6 +5,7 @@ using Discord.Net.Converters; | |||||
| using Discord.Net.Queue; | using Discord.Net.Queue; | ||||
| using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -21,133 +22,96 @@ using System.Threading.Tasks; | |||||
| namespace Discord.API | namespace Discord.API | ||||
| { | { | ||||
| public class DiscordApiClient : IDisposable | |||||
| public class DiscordRestApiClient : IDisposable | |||||
| { | { | ||||
| private object _eventLock = new object(); | |||||
| public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } | ||||
| private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>(); | ||||
| public event Func<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>(); | |||||
| public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, object, 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 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 LoginState LoginState { get; private set; } | ||||
| public ConnectionState ConnectionState { get; private set; } | |||||
| public TokenType AuthTokenType { 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("accept", "*/*"); | ||||
| _restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent); | _restClient.SetHeader("user-agent", DiscordRestConfig.UserAgent); | ||||
| if (webSocketProvider != null) | |||||
| _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); | |||||
| } | |||||
| internal static string GetPrefixedToken(TokenType tokenType, string token) | |||||
| { | |||||
| switch (tokenType) | |||||
| { | { | ||||
| _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)) | |||||
| { | |||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd()); | |||||
| await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| }; | |||||
| _gatewayClient.TextMessage += async text => | |||||
| { | |||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | |||||
| 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); | |||||
| }; | |||||
| 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)); | |||||
| } | } | ||||
| _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
| } | } | ||||
| private void Dispose(bool disposing) | |||||
| internal virtual void Dispose(bool disposing) | |||||
| { | { | ||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| _loginCancelToken?.Dispose(); | _loginCancelToken?.Dispose(); | ||||
| _connectCancelToken?.Dispose(); | |||||
| (_restClient as IDisposable)?.Dispose(); | (_restClient as IDisposable)?.Dispose(); | ||||
| (_gatewayClient as IDisposable)?.Dispose(); | |||||
| } | } | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
| public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) | public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| await _stateLock.WaitAsync().ConfigureAwait(false); | |||||
| try | try | ||||
| { | { | ||||
| await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); | await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); | ||||
| } | } | ||||
| finally { _connectionLock.Release(); } | |||||
| finally { _stateLock.Release(); } | |||||
| } | } | ||||
| private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) | private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) | ||||
| { | { | ||||
| if (LoginState != LoginState.LoggedOut) | if (LoginState != LoginState.LoggedOut) | ||||
| await LogoutInternalAsync().ConfigureAwait(false); | await LogoutInternalAsync().ConfigureAwait(false); | ||||
| LoginState = LoginState.LoggingIn; | LoginState = LoginState.LoggingIn; | ||||
| try | try | ||||
| { | { | ||||
| _loginCancelToken = new CancellationTokenSource(); | _loginCancelToken = new CancellationTokenSource(); | ||||
| AuthTokenType = TokenType.User; | AuthTokenType = TokenType.User; | ||||
| _authToken = null; | _authToken = null; | ||||
| _restClient.SetHeader("authorization", null); | |||||
| await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); | |||||
| await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); | |||||
| _restClient.SetCancelToken(_loginCancelToken.Token); | _restClient.SetCancelToken(_loginCancelToken.Token); | ||||
| AuthTokenType = tokenType; | AuthTokenType = tokenType; | ||||
| _authToken = token; | _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; | LoginState = LoginState.LoggedIn; | ||||
| } | } | ||||
| @@ -160,129 +124,58 @@ namespace Discord.API | |||||
| public async Task LogoutAsync() | public async Task LogoutAsync() | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| await _stateLock.WaitAsync().ConfigureAwait(false); | |||||
| try | try | ||||
| { | { | ||||
| await LogoutInternalAsync().ConfigureAwait(false); | await LogoutInternalAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| finally { _connectionLock.Release(); } | |||||
| finally { _stateLock.Release(); } | |||||
| } | } | ||||
| private async Task LogoutInternalAsync() | 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. | //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; | if (LoginState == LoginState.LoggedOut) return; | ||||
| LoginState = LoginState.LoggingOut; | LoginState = LoginState.LoggingOut; | ||||
| try { _loginCancelToken?.Cancel(false); } | try { _loginCancelToken?.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | 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); | _restClient.SetCancelToken(CancellationToken.None); | ||||
| LoginState = LoginState.LoggedOut; | 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 | //REST | ||||
| public Task SendAsync(string method, string endpoint, | |||||
| public Task SendAsync(string method, string endpoint, | |||||
| GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) | GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) | ||||
| => SendInternalAsync(method, endpoint, null, true, BucketGroup.Global, (int)bucket, 0, options); | => 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) | GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) | ||||
| => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Global, (int)bucket, 0, options); | => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Global, (int)bucket, 0, options); | ||||
| public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, | public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint, | ||||
| GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class | 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)); | => 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 | GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class | ||||
| => DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Global, (int)bucket, 0, options).ConfigureAwait(false)); | => 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) | GuildBucket bucket, ulong guildId, RequestOptions options = null) | ||||
| => SendInternalAsync(method, endpoint, null, true, BucketGroup.Guild, (int)bucket, guildId, options); | => 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) | GuildBucket bucket, ulong guildId, RequestOptions options = null) | ||||
| => SendInternalAsync(method, endpoint, payload, true, BucketGroup.Guild, (int)bucket, guildId, options); | => 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 | 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)); | => 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 | 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)); | => DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, payload, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); | ||||
| @@ -301,24 +194,15 @@ namespace Discord.API | |||||
| GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class | GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class | ||||
| => DeserializeJson<TResponse>(await SendMultipartInternalAsync(method, endpoint, multipartArgs, false, BucketGroup.Guild, (int)bucket, guildId, options).ConfigureAwait(false)); | => DeserializeJson<TResponse>(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 | //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) | BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) | ||||
| { | { | ||||
| var stopwatch = Stopwatch.StartNew(); | var stopwatch = Stopwatch.StartNew(); | ||||
| string json = null; | string json = null; | ||||
| if (payload != null) | if (payload != null) | ||||
| json = SerializeJson(payload); | 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(); | stopwatch.Stop(); | ||||
| double milliseconds = ToMilliseconds(stopwatch); | double milliseconds = ToMilliseconds(stopwatch); | ||||
| @@ -326,11 +210,11 @@ namespace Discord.API | |||||
| return responseStream; | 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) | BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) | ||||
| { | { | ||||
| var stopwatch = Stopwatch.StartNew(); | 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; | int bytes = headerOnly ? 0 : (int)responseStream.Length; | ||||
| stopwatch.Stop(); | stopwatch.Stop(); | ||||
| @@ -339,23 +223,6 @@ namespace Discord.API | |||||
| return responseStream; | 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); | |||||
| } | |||||
| //Application | |||||
| public async Task<Application> GetMyApplicationInfoAsync(RequestOptions options = null) | |||||
| { | |||||
| return await SendAsync<Application>("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false); | |||||
| } | |||||
| //Auth | //Auth | ||||
| public async Task ValidateTokenAsync(RequestOptions options = null) | public async Task ValidateTokenAsync(RequestOptions options = null) | ||||
| @@ -363,69 +230,6 @@ namespace Discord.API | |||||
| await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false); | await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| //Gateway | |||||
| public async Task<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null) | |||||
| { | |||||
| return await SendAsync<GetGatewayResponse>("GET", "gateway", options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) | |||||
| { | |||||
| var props = new Dictionary<string, string> | |||||
| { | |||||
| ["$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<ulong> 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<ulong> guildIds, RequestOptions options = null) | |||||
| { | |||||
| await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); | |||||
| } | |||||
| //Channels | //Channels | ||||
| public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null) | ||||
| { | { | ||||
| @@ -791,13 +595,13 @@ namespace Discord.API | |||||
| List<GuildMember[]> result; | List<GuildMember[]> result; | ||||
| if (args._limit.IsSpecified) | if (args._limit.IsSpecified) | ||||
| result = new List<GuildMember[]>((limit + DiscordRestConfig.MaxUsersPerBatch - 1) / DiscordRestConfig.MaxUsersPerBatch); | |||||
| result = new List<GuildMember[]>((limit + DiscordConfig.MaxUsersPerBatch - 1) / DiscordConfig.MaxUsersPerBatch); | |||||
| else | else | ||||
| result = new List<GuildMember[]>(); | result = new List<GuildMember[]>(); | ||||
| while (true) | 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}"; | string endpoint = $"guilds/{guildId}/members?limit={runLimit}&after={afterUserId}"; | ||||
| var models = await SendAsync<GuildMember[]>("GET", endpoint, options: options).ConfigureAwait(false); | var models = await SendAsync<GuildMember[]>("GET", endpoint, options: options).ConfigureAwait(false); | ||||
| @@ -806,11 +610,11 @@ namespace Discord.API | |||||
| result.Add(models); | result.Add(models); | ||||
| limit -= DiscordRestConfig.MaxUsersPerBatch; | |||||
| limit -= DiscordConfig.MaxUsersPerBatch; | |||||
| afterUserId = models[models.Length - 1].User.Id; | afterUserId = models[models.Length - 1].User.Id; | ||||
| //Was this an incomplete (the last) batch? | //Was this an incomplete (the last) batch? | ||||
| if (models.Length != DiscordRestConfig.MaxUsersPerBatch) break; | |||||
| if (models.Length != DiscordConfig.MaxUsersPerBatch) break; | |||||
| } | } | ||||
| if (result.Count > 1) | if (result.Count > 1) | ||||
| @@ -919,15 +723,15 @@ namespace Discord.API | |||||
| relativeDir = "around"; | relativeDir = "around"; | ||||
| break; | 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][]; | var result = new API.Message[runs][]; | ||||
| int i = 0; | int i = 0; | ||||
| for (; i < runs; i++) | for (; i < runs; i++) | ||||
| { | { | ||||
| int runCount = i == (runs - 1) ? lastRunCount : DiscordRestConfig.MaxMessagesPerBatch; | |||||
| int runCount = i == (runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; | |||||
| string endpoint; | string endpoint; | ||||
| if (relativeId != null) | if (relativeId != null) | ||||
| endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; | endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; | ||||
| @@ -966,7 +770,7 @@ namespace Discord.API | |||||
| } | } | ||||
| //Was this an incomplete (the last) batch? | //Was this an incomplete (the last) batch? | ||||
| if (models.Length != DiscordRestConfig.MaxMessagesPerBatch) { i++; break; } | |||||
| if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } | |||||
| } | } | ||||
| if (i > 1) | if (i > 1) | ||||
| @@ -1011,8 +815,8 @@ namespace Discord.API | |||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | Preconditions.NotEqual(channelId, 0, nameof(channelId)); | ||||
| Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
| Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); | 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) | if (guildId != 0) | ||||
| return await SendAsync<Message>("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); | return await SendAsync<Message>("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); | ||||
| @@ -1033,15 +837,15 @@ namespace Discord.API | |||||
| { | { | ||||
| Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | Preconditions.NotEqual(channelId, 0, nameof(channelId)); | ||||
| if (args._content.GetValueOrDefault(null) == null) | if (args._content.GetValueOrDefault(null) == null) | ||||
| args._content = ""; | args._content = ""; | ||||
| else if (args._content.IsSpecified) | else if (args._content.IsSpecified) | ||||
| { | { | ||||
| if (args._content.Value == null) | if (args._content.Value == null) | ||||
| args._content = ""; | 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) | if (guildId != 0) | ||||
| @@ -1121,8 +925,8 @@ namespace Discord.API | |||||
| if (args._content.IsSpecified) | if (args._content.IsSpecified) | ||||
| { | { | ||||
| Preconditions.NotNullOrEmpty(args._content, nameof(args.Content)); | 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) | if (guildId != 0) | ||||
| @@ -1159,7 +963,7 @@ namespace Discord.API | |||||
| { | { | ||||
| Preconditions.NotNullOrEmpty(username, nameof(username)); | Preconditions.NotNullOrEmpty(username, nameof(username)); | ||||
| Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); | Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); | ||||
| try | try | ||||
| { | { | ||||
| var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); | var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); | ||||
| @@ -1176,7 +980,7 @@ namespace Discord.API | |||||
| } | } | ||||
| //Current User/DMs | //Current User/DMs | ||||
| public async Task<User> GetSelfAsync(RequestOptions options = null) | |||||
| public async Task<User> GetMyUserAsync(RequestOptions options = null) | |||||
| { | { | ||||
| return await SendAsync<User>("GET", "users/@me", options: options).ConfigureAwait(false); | return await SendAsync<User>("GET", "users/@me", options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -1192,6 +996,10 @@ namespace Discord.API | |||||
| { | { | ||||
| return await SendAsync<IReadOnlyCollection<UserGuild>>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); | return await SendAsync<IReadOnlyCollection<UserGuild>>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task<Application> GetMyApplicationAsync(RequestOptions options = null) | |||||
| { | |||||
| return await SendAsync<Application>("GET", "oauth2/applications/@me", options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<User> ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) | public async Task<User> ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) | ||||
| { | { | ||||
| Preconditions.NotNull(args, nameof(args)); | Preconditions.NotNull(args, nameof(args)); | ||||
| @@ -1227,8 +1035,8 @@ namespace Discord.API | |||||
| } | } | ||||
| //Helpers | //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); | var sb = new StringBuilder(256); | ||||
| using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) | using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) | ||||
| @@ -1236,7 +1044,7 @@ namespace Discord.API | |||||
| _serializer.Serialize(writer, value); | _serializer.Serialize(writer, value); | ||||
| return sb.ToString(); | return sb.ToString(); | ||||
| } | } | ||||
| private T DeserializeJson<T>(Stream jsonStream) | |||||
| protected T DeserializeJson<T>(Stream jsonStream) | |||||
| { | { | ||||
| using (TextReader text = new StreamReader(jsonStream)) | using (TextReader text = new StreamReader(jsonStream)) | ||||
| using (JsonReader reader = new JsonTextReader(text)) | using (JsonReader reader = new JsonTextReader(text)) | ||||
| @@ -0,0 +1,360 @@ | |||||
| using Discord.API.Rpc; | |||||
| using Discord.Net.Queue; | |||||
| using Discord.Net.Rest; | |||||
| using Discord.Net.WebSockets; | |||||
| using Discord.Rpc; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.IO; | |||||
| using System.IO.Compression; | |||||
| using System.Text; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API | |||||
| { | |||||
| public class DiscordRpcApiClient : DiscordRestApiClient, IDisposable | |||||
| { | |||||
| private abstract class RpcRequest | |||||
| { | |||||
| public abstract Task SetResultAsync(JToken data, JsonSerializer serializer); | |||||
| public abstract Task SetExceptionAsync(JToken data, JsonSerializer serializer); | |||||
| } | |||||
| private class RpcRequest<T> : RpcRequest | |||||
| { | |||||
| public TaskCompletionSource<T> Promise { get; set; } | |||||
| public RpcRequest(RequestOptions options) | |||||
| { | |||||
| Promise = new TaskCompletionSource<T>(); | |||||
| 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<T>(serializer)); | |||||
| } | |||||
| public override Task SetExceptionAsync(JToken data, JsonSerializer serializer) | |||||
| { | |||||
| var error = data.ToObject<ErrorEvent>(serializer); | |||||
| return Promise.TrySetExceptionAsync(new RpcException(error.Code, error.Message)); | |||||
| } | |||||
| } | |||||
| 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, Optional<string>, Optional<object>, Task> ReceivedRpcEvent { add { _receivedRpcEvent.Add(value); } remove { _receivedRpcEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<string, Optional<string>, Optional<object>, Task>> _receivedRpcEvent = new AsyncEvent<Func<string, Optional<string>, Optional<object>, 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 ConcurrentDictionary<Guid, RpcRequest> _requests; | |||||
| private readonly RequestQueue _requestQueue; | |||||
| private readonly IWebSocketClient _webSocketClient; | |||||
| private readonly SemaphoreSlim _connectionLock; | |||||
| private readonly string _clientId; | |||||
| private CancellationTokenSource _loginCancelToken, _connectCancelToken; | |||||
| private string _origin; | |||||
| public ConnectionState ConnectionState { get; private set; } | |||||
| 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; | |||||
| _origin = origin; | |||||
| _requestQueue = requestQueue ?? new RequestQueue(); | |||||
| _requests = new ConcurrentDictionary<Guid, RpcRequest>(); | |||||
| _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 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<API.Rpc.RpcMessage>(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 jsonReader = new JsonTextReader(reader)) | |||||
| { | |||||
| var msg = _serializer.Deserialize<API.Rpc.RpcMessage>(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); | |||||
| }; | |||||
| } | |||||
| internal override void Dispose(bool disposing) | |||||
| { | |||||
| if (!_isDisposed) | |||||
| { | |||||
| if (disposing) | |||||
| { | |||||
| _connectCancelToken?.Dispose(); | |||||
| (_webSocketClient as IDisposable)?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | |||||
| } | |||||
| } | |||||
| public async Task ConnectAsync() | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await ConnectInternalAsync().ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| internal override 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; | |||||
| int port; | |||||
| string uuid = Guid.NewGuid().ToString(); | |||||
| for ( port = DiscordRpcConfig.PortRangeStart; port <= DiscordRpcConfig.PortRangeEnd; port++) | |||||
| { | |||||
| try | |||||
| { | |||||
| string url = $"wss://{uuid}.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."); | |||||
| SetBaseUrl($"https://{uuid}.discordapp.io:{port}/"); | |||||
| 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(); } | |||||
| } | |||||
| internal override 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<TResponse> SendRpcAsync<TResponse>(string cmd, object payload, GlobalBucket bucket = GlobalBucket.GeneralRpc, | |||||
| Optional<string> evt = default(Optional<string>), RequestOptions options = null) | |||||
| where TResponse : class | |||||
| => SendRpcAsyncInternal<TResponse>(cmd, payload, BucketGroup.Global, (int)bucket, 0, evt, options); | |||||
| public Task<TResponse> SendRpcAsync<TResponse>(string cmd, object payload, GuildBucket bucket, ulong guildId, | |||||
| Optional<string> evt = default(Optional<string>), RequestOptions options = null) | |||||
| where TResponse : class | |||||
| => SendRpcAsyncInternal<TResponse>(cmd, payload, BucketGroup.Guild, (int)bucket, guildId, evt, options); | |||||
| private async Task<TResponse> SendRpcAsyncInternal<TResponse>(string cmd, object payload, BucketGroup group, int bucketId, ulong guildId, | |||||
| Optional<string> evt, RequestOptions options) | |||||
| where TResponse : class | |||||
| { | |||||
| byte[] bytes = null; | |||||
| var guid = Guid.NewGuid(); | |||||
| payload = new API.Rpc.RpcMessage { Cmd = cmd, Event = evt, Args = payload, Nonce = guid }; | |||||
| if (payload != null) | |||||
| { | |||||
| var json = SerializeJson(payload); | |||||
| bytes = Encoding.UTF8.GetBytes(json); | |||||
| } | |||||
| var requestTracker = new RpcRequest<TResponse>(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<AuthenticateResponse> SendAuthenticateAsync(RequestOptions options = null) | |||||
| { | |||||
| var msg = new AuthenticateParams | |||||
| { | |||||
| AccessToken = _authToken | |||||
| }; | |||||
| return await SendRpcAsync<AuthenticateResponse>("AUTHENTICATE", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<AuthorizeResponse> SendAuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null) | |||||
| { | |||||
| var msg = new AuthorizeParams | |||||
| { | |||||
| ClientId = _clientId, | |||||
| Scopes = scopes, | |||||
| RpcToken = rpcToken != null ? rpcToken : Optional.Create<string>() | |||||
| }; | |||||
| 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<AuthorizeResponse>("AUTHORIZE", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<GetGuildsResponse> SendGetGuildsAsync(RequestOptions options = null) | |||||
| { | |||||
| return await SendRpcAsync<GetGuildsResponse>("GET_GUILDS", null, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<RpcGuild> SendGetGuildAsync(ulong guildId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new GetGuildParams | |||||
| { | |||||
| GuildId = guildId | |||||
| }; | |||||
| return await SendRpcAsync<RpcGuild>("GET_GUILD", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<GetChannelsResponse> SendGetChannelsAsync(ulong guildId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new GetChannelsParams | |||||
| { | |||||
| GuildId = guildId | |||||
| }; | |||||
| return await SendRpcAsync<GetChannelsResponse>("GET_CHANNELS", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<RpcChannel> SendGetChannelAsync(ulong channelId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new GetChannelParams | |||||
| { | |||||
| ChannelId = channelId | |||||
| }; | |||||
| return await SendRpcAsync<RpcChannel>("GET_CHANNEL", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<SetLocalVolumeResponse> SendSetLocalVolumeAsync(int volume, RequestOptions options = null) | |||||
| { | |||||
| var msg = new SetLocalVolumeParams | |||||
| { | |||||
| Volume = volume | |||||
| }; | |||||
| return await SendRpcAsync<SetLocalVolumeResponse>("SET_LOCAL_VOLUME", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<RpcChannel> SendSelectVoiceChannelAsync(ulong channelId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new SelectVoiceChannelParams | |||||
| { | |||||
| ChannelId = channelId | |||||
| }; | |||||
| return await SendRpcAsync<RpcChannel>("SELECT_VOICE_CHANNEL", msg, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<SubscriptionResponse> SendChannelSubscribeAsync(string evt, ulong channelId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new ChannelSubscriptionParams | |||||
| { | |||||
| ChannelId = channelId | |||||
| }; | |||||
| return await SendRpcAsync<SubscriptionResponse>("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<SubscriptionResponse> SendChannelUnsubscribeAsync(string evt, ulong channelId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new ChannelSubscriptionParams | |||||
| { | |||||
| ChannelId = channelId | |||||
| }; | |||||
| return await SendRpcAsync<SubscriptionResponse>("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<SubscriptionResponse> SendGuildSubscribeAsync(string evt, ulong guildId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new GuildSubscriptionParams | |||||
| { | |||||
| GuildId = guildId | |||||
| }; | |||||
| return await SendRpcAsync<SubscriptionResponse>("SUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<SubscriptionResponse> SendGuildUnsubscribeAsync(string evt, ulong guildId, RequestOptions options = null) | |||||
| { | |||||
| var msg = new GuildSubscriptionParams | |||||
| { | |||||
| GuildId = guildId | |||||
| }; | |||||
| return await SendRpcAsync<SubscriptionResponse>("UNSUBSCRIBE", msg, evt: evt, options: options).ConfigureAwait(false); | |||||
| } | |||||
| private bool ProcessMessage(API.Rpc.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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,243 @@ | |||||
| 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<GatewayOpCode, Task> SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<GatewayOpCode, Task>> _sentGatewayMessageEvent = new AsyncEvent<Func<GatewayOpCode, Task>>(); | |||||
| public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } | |||||
| private readonly AsyncEvent<Func<GatewayOpCode, int?, string, object, Task>> _receivedGatewayEvent = new AsyncEvent<Func<GatewayOpCode, int?, string, object, 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 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<WebSocketMessage>(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<WebSocketMessage>(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<GetGatewayResponse> GetGatewayAsync(RequestOptions options = null) | |||||
| { | |||||
| return await SendAsync<GetGatewayResponse>("GET", "gateway", options: options).ConfigureAwait(false); | |||||
| } | |||||
| public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, int shardID = 0, int totalShards = 1, RequestOptions options = null) | |||||
| { | |||||
| var props = new Dictionary<string, string> | |||||
| { | |||||
| ["$device"] = "Discord.Net" | |||||
| }; | |||||
| var msg = new IdentifyParams() | |||||
| { | |||||
| Token = _authToken, | |||||
| Properties = props, | |||||
| LargeThreshold = largeThreshold, | |||||
| UseCompression = useCompression, | |||||
| }; | |||||
| if (totalShards > 1) | |||||
| msg.ShardingParams = new int[] { shardID, totalShards }; | |||||
| 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<ulong> 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<ulong> guildIds, RequestOptions options = null) | |||||
| { | |||||
| await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -14,5 +14,7 @@ namespace Discord.API.Gateway | |||||
| public int LargeThreshold { get; set; } | public int LargeThreshold { get; set; } | ||||
| [JsonProperty("compress")] | [JsonProperty("compress")] | ||||
| public bool UseCompression { get; set; } | public bool UseCompression { get; set; } | ||||
| [JsonProperty("shard")] | |||||
| public Optional<int[]> ShardingParams { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -13,7 +13,7 @@ namespace Discord.API.Gateway | |||||
| public int Limit { get; set; } | public int Limit { get; set; } | ||||
| [JsonProperty("guild_id")] | [JsonProperty("guild_id")] | ||||
| private ulong[] _guildIds; | |||||
| private ulong[] _guildIds { get; set; } | |||||
| public IEnumerable<ulong> GuildIds { set { _guildIds = value.ToArray(); } } | public IEnumerable<ulong> GuildIds { set { _guildIds = value.ToArray(); } } | ||||
| public IEnumerable<IGuild> Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } } | public IEnumerable<IGuild> Guilds { set { _guildIds = value.Select(x => x.Id).ToArray(); } } | ||||
| } | } | ||||
| @@ -1,8 +1,10 @@ | |||||
| namespace Discord.API.Rest | |||||
| using Discord.Rest; | |||||
| namespace Discord.API.Rest | |||||
| { | { | ||||
| public class GetChannelMessagesParams | 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; | public Direction RelativeDirection { internal get; set; } = Direction.Before; | ||||
| @@ -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,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class AuthenticateParams | |||||
| { | |||||
| [JsonProperty("access_token")] | |||||
| public string AccessToken { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class AuthenticateResponse | |||||
| { | |||||
| [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,14 @@ | |||||
| 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; } | |||||
| [JsonProperty("rpc_token")] | |||||
| public Optional<string> RpcToken { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class AuthorizeResponse | |||||
| { | |||||
| [JsonProperty("code")] | |||||
| public string Code { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class ChannelSubscriptionParams | |||||
| { | |||||
| [JsonProperty("channel_id")] | |||||
| public ulong ChannelId { 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,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetChannelParams | |||||
| { | |||||
| [JsonProperty("channel_id")] | |||||
| public ulong ChannelId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetChannelsParams | |||||
| { | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong GuildId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetChannelsResponse | |||||
| { | |||||
| [JsonProperty("channels")] | |||||
| public RpcChannel[] Channels { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetGuildParams | |||||
| { | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong GuildId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetGuildsParams | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GetGuildsResponse | |||||
| { | |||||
| [JsonProperty("guilds")] | |||||
| public RpcUserGuild[] Guilds { 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,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class GuildSubscriptionParams | |||||
| { | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong GuildId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| 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; } | |||||
| } | |||||
| } | |||||
| @@ -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,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class RpcChannel : Channel | |||||
| { | |||||
| [JsonProperty("voice_states")] | |||||
| public VoiceState[] VoiceStates { 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,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; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class RpcMessage | |||||
| { | |||||
| [JsonProperty("cmd")] | |||||
| public string Cmd { get; set; } | |||||
| [JsonProperty("nonce")] | |||||
| public Optional<Guid?> Nonce { get; set; } | |||||
| [JsonProperty("evt")] | |||||
| public Optional<string> Event { get; set; } | |||||
| [JsonProperty("data")] | |||||
| public Optional<object> Data { get; set; } | |||||
| [JsonProperty("args")] | |||||
| public object Args { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class SelectVoiceChannelParams | |||||
| { | |||||
| [JsonProperty("channel_id")] | |||||
| public ulong? ChannelId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class SetLocalVolumeParams | |||||
| { | |||||
| [JsonProperty("volume")] | |||||
| public int Volume { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -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; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class SpeakingEvent | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class SubscriptionResponse | |||||
| { | |||||
| [JsonProperty("evt")] | |||||
| public string Event { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.API.Rpc | |||||
| { | |||||
| public class VoiceStateEvent | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -1,6 +1,7 @@ | |||||
| using Discord.API.Voice; | using Discord.API.Voice; | ||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
| using Discord.WebSocket; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| @@ -1,5 +1,4 @@ | |||||
| using Discord.Extensions; | |||||
| using System; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| @@ -5,12 +5,19 @@ namespace Discord | |||||
| public class DiscordConfig | public class DiscordConfig | ||||
| { | { | ||||
| public const int APIVersion = 6; | public const int APIVersion = 6; | ||||
| public static string Version { get; } = typeof(DiscordRestConfig).GetTypeInfo().Assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion ?? "Unknown"; | |||||
| public static string Version { get; } = | |||||
| typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? | |||||
| typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? | |||||
| "Unknown"; | |||||
| public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; | public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; | ||||
| public const string CDNUrl = "https://cdn.discordapp.com/"; | public const string CDNUrl = "https://cdn.discordapp.com/"; | ||||
| public const string InviteUrl = "https://discord.gg/"; | public const string InviteUrl = "https://discord.gg/"; | ||||
| public const int MaxMessageSize = 2000; | |||||
| public const int MaxMessagesPerBatch = 100; | |||||
| public const int MaxUsersPerBatch = 1000; | |||||
| /// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> | /// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> | ||||
| public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | public LogSeverity LogLevel { get; set; } = LogSeverity.Info; | ||||
| } | } | ||||
| @@ -20,9 +20,9 @@ namespace Discord | |||||
| /// <summary> Gets the message from this channel's cache with the given id, or null if not found. </summary> | /// <summary> Gets the message from this channel's cache with the given id, or null if not found. </summary> | ||||
| IMessage GetCachedMessage(ulong id); | IMessage GetCachedMessage(ulong id); | ||||
| /// <summary> Gets the last N messages from this message channel. </summary> | /// <summary> Gets the last N messages from this message channel. </summary> | ||||
| Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch); | |||||
| Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Gets a collection of messages in this channel. </summary> | /// <summary> Gets a collection of messages in this channel. </summary> | ||||
| Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch); | |||||
| Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Bulk deletes multiple messages. </summary> | /// <summary> Bulk deletes multiple messages. </summary> | ||||
| Task DeleteMessagesAsync(IEnumerable<IMessage> messages); | Task DeleteMessagesAsync(IEnumerable<IMessage> messages); | ||||
| @@ -1,21 +0,0 @@ | |||||
| using System; | |||||
| using Model = Discord.API.User; | |||||
| namespace Discord | |||||
| { | |||||
| internal class SocketSelfUser : SelfUser, ISocketUser | |||||
| { | |||||
| internal override bool IsAttached => true; | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| SocketGlobalUser ISocketUser.User { get { throw new NotSupportedException(); } } | |||||
| public SocketSelfUser(DiscordSocketClient discord, Model model) | |||||
| : base(discord, model) | |||||
| { | |||||
| } | |||||
| public SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; | |||||
| ISocketUser ISocketUser.Clone() => Clone(); | |||||
| } | |||||
| } | |||||
| @@ -4,7 +4,7 @@ using System.Collections.Generic; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| namespace Discord.Extensions | |||||
| namespace Discord | |||||
| { | { | ||||
| internal static class CollectionExtensions | internal static class CollectionExtensions | ||||
| { | { | ||||
| @@ -1,7 +1,8 @@ | |||||
| using System.Linq; | |||||
| using Discord.Rest; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Extensions | |||||
| namespace Discord | |||||
| { | { | ||||
| public static class DiscordClientExtensions | public static class DiscordClientExtensions | ||||
| { | { | ||||
| @@ -1,6 +1,6 @@ | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Extensions | |||||
| namespace Discord | |||||
| { | { | ||||
| public static class GuildExtensions | public static class GuildExtensions | ||||
| { | { | ||||
| @@ -2,7 +2,7 @@ | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Extensions | |||||
| namespace Discord | |||||
| { | { | ||||
| public static class GuildUserExtensions | public static class GuildUserExtensions | ||||
| { | { | ||||
| @@ -13,7 +13,7 @@ namespace Discord | |||||
| { | { | ||||
| ConnectionState ConnectionState { get; } | ConnectionState ConnectionState { get; } | ||||
| DiscordApiClient ApiClient { get; } | |||||
| DiscordRestApiClient ApiClient { get; } | |||||
| ILogManager LogManager { get; } | ILogManager LogManager { get; } | ||||
| Task ConnectAsync(); | Task ConnectAsync(); | ||||
| @@ -5,7 +5,10 @@ | |||||
| GeneralRest, | GeneralRest, | ||||
| DirectMessage, | DirectMessage, | ||||
| SendEditMessage, | SendEditMessage, | ||||
| GeneralGateway, | GeneralGateway, | ||||
| UpdateStatus | |||||
| UpdateStatus, | |||||
| GeneralRpc | |||||
| } | } | ||||
| } | } | ||||
| @@ -36,7 +36,10 @@ namespace Discord.Net.Queue | |||||
| //Gateway | //Gateway | ||||
| [GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both), | [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(); | }.ToImmutableDictionary(); | ||||
| _guildLimits = new Dictionary<GuildBucket, Bucket> | _guildLimits = new Dictionary<GuildBucket, Bucket> | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -10,8 +10,10 @@ using System.Linq; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using System.Runtime.InteropServices; | using System.Runtime.InteropServices; | ||||
| using Discord.Rpc; | |||||
| using Discord.WebSocket; | |||||
| namespace Discord | |||||
| namespace Discord.Rest | |||||
| { | { | ||||
| public class DiscordRestClient : IDiscordClient | public class DiscordRestClient : IDiscordClient | ||||
| { | { | ||||
| @@ -27,20 +29,21 @@ namespace Discord | |||||
| internal readonly ILogger _clientLogger, _restLogger, _queueLogger; | internal readonly ILogger _clientLogger, _restLogger, _queueLogger; | ||||
| internal readonly SemaphoreSlim _connectionLock; | internal readonly SemaphoreSlim _connectionLock; | ||||
| internal readonly RequestQueue _requestQueue; | |||||
| internal bool _isDisposed; | |||||
| internal SelfUser _currentUser; | internal SelfUser _currentUser; | ||||
| private bool _isFirstLogSub; | private bool _isFirstLogSub; | ||||
| internal bool _isDisposed; | |||||
| public API.DiscordApiClient ApiClient { get; } | |||||
| public API.DiscordRestApiClient ApiClient { get; } | |||||
| internal LogManager LogManager { get; } | internal LogManager LogManager { get; } | ||||
| public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
| /// <summary> Creates a new REST-only discord client. </summary> | /// <summary> Creates a new REST-only discord client. </summary> | ||||
| public DiscordRestClient() : this(new DiscordRestConfig()) { } | public DiscordRestClient() : this(new DiscordRestConfig()) { } | ||||
| public DiscordRestClient(DiscordRestConfig config) : this(config, CreateApiClient(config)) { } | |||||
| /// <summary> Creates a new REST-only discord client. </summary> | /// <summary> Creates a new REST-only discord client. </summary> | ||||
| public DiscordRestClient(DiscordRestConfig config) | |||||
| internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient client) | |||||
| { | { | ||||
| ApiClient = client; | |||||
| LogManager = new LogManager(config.LogLevel); | LogManager = new LogManager(config.LogLevel); | ||||
| LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | ||||
| _clientLogger = LogManager.CreateLogger("Client"); | _clientLogger = LogManager.CreateLogger("Client"); | ||||
| @@ -50,19 +53,16 @@ namespace Discord | |||||
| _connectionLock = new SemaphoreSlim(1, 1); | _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); | await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); | ||||
| if (bucket == null && id != null) | if (bucket == null && id != null) | ||||
| await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); | 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); | 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()); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) | public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) | ||||
| @@ -89,20 +89,9 @@ namespace Discord | |||||
| try | try | ||||
| { | { | ||||
| await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); | await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); | ||||
| if (validateToken) | if (validateToken) | ||||
| { | |||||
| try | |||||
| { | |||||
| await ApiClient.ValidateTokenAsync().ConfigureAwait(false); | |||||
| } | |||||
| 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; | LoginState = LoginState.LoggedIn; | ||||
| } | } | ||||
| @@ -114,7 +103,26 @@ namespace Discord | |||||
| await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | 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; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task LogoutAsync() | public async Task LogoutAsync() | ||||
| @@ -146,7 +154,7 @@ namespace Discord | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public async Task<IApplication> GetApplicationInfoAsync() | public async Task<IApplication> GetApplicationInfoAsync() | ||||
| { | { | ||||
| var model = await ApiClient.GetMyApplicationInfoAsync().ConfigureAwait(false); | |||||
| var model = await ApiClient.GetMyApplicationAsync().ConfigureAwait(false); | |||||
| return new Application(this, model); | return new Application(this, model); | ||||
| } | } | ||||
| @@ -266,12 +274,13 @@ namespace Discord | |||||
| var user = _currentUser; | var user = _currentUser; | ||||
| if (user == null) | if (user == null) | ||||
| { | { | ||||
| var model = await ApiClient.GetSelfAsync().ConfigureAwait(false); | |||||
| var model = await ApiClient.GetMyUserAsync().ConfigureAwait(false); | |||||
| user = new SelfUser(this, model); | user = new SelfUser(this, model); | ||||
| _currentUser = user; | _currentUser = user; | ||||
| } | } | ||||
| return user; | return user; | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public virtual async Task<IReadOnlyCollection<IUser>> QueryUsersAsync(string query, int limit) | public virtual async Task<IReadOnlyCollection<IUser>> QueryUsersAsync(string query, int limit) | ||||
| { | { | ||||
| @@ -295,8 +304,10 @@ namespace Discord | |||||
| internal virtual void Dispose(bool disposing) | internal virtual void Dispose(bool disposing) | ||||
| { | { | ||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | |||||
| ApiClient.Dispose(); | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| ApiClient.Dispose(); | |||||
| } | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
| @@ -305,9 +316,10 @@ namespace Discord | |||||
| { | { | ||||
| if (this is DiscordSocketClient) | 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}, {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 | 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($"Runtime: {RuntimeInformation.FrameworkDescription.Trim()} ({ToArchString(RuntimeInformation.ProcessArchitecture)})").ConfigureAwait(false); | ||||
| await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | await _clientLogger.VerboseAsync($"OS: {RuntimeInformation.OSDescription.Trim()} ({ToArchString(RuntimeInformation.OSArchitecture)})").ConfigureAwait(false); | ||||
| await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | await _clientLogger.VerboseAsync($"Processors: {Environment.ProcessorCount}").ConfigureAwait(false); | ||||
| @@ -1,15 +1,11 @@ | |||||
| using Discord.Net.Rest; | using Discord.Net.Rest; | ||||
| namespace Discord | |||||
| namespace Discord.Rest | |||||
| { | { | ||||
| public class DiscordRestConfig : DiscordConfig | public class DiscordRestConfig : DiscordConfig | ||||
| { | { | ||||
| public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; | 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 RestTimeout = 10000; | ||||
| internal const int MessageQueueInterval = 100; | internal const int MessageQueueInterval = 100; | ||||
| internal const int WebSocketQueueInterval = 100; | internal const int WebSocketQueueInterval = 100; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Application; | using Model = Discord.API.Application; | ||||
| @@ -42,7 +43,7 @@ namespace Discord | |||||
| { | { | ||||
| if (IsAttached) throw new NotSupportedException(); | 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) | if (response.Id != Id) | ||||
| throw new InvalidOperationException("Unable to update this object from a different application token."); | throw new InvalidOperationException("Unable to update this object from a different application token."); | ||||
| Update(response, UpdateSource.Rest); | Update(response, UpdateSource.Rest); | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -1,5 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Extensions; | |||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -17,7 +17,7 @@ namespace Discord | |||||
| { | { | ||||
| protected ConcurrentDictionary<ulong, GroupUser> _users; | protected ConcurrentDictionary<ulong, GroupUser> _users; | ||||
| private string _iconId; | private string _iconId; | ||||
| public override DiscordRestClient Discord { get; } | public override DiscordRestClient Discord { get; } | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -1,4 +1,6 @@ | |||||
| namespace Discord | |||||
| using Discord.Rest; | |||||
| namespace Discord | |||||
| { | { | ||||
| internal abstract class Entity<T> : IEntity<T> | internal abstract class Entity<T> : IEntity<T> | ||||
| { | { | ||||
| @@ -1,6 +1,6 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Audio; | using Discord.Audio; | ||||
| using Discord.Extensions; | |||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using Discord.Rest; | |||||
| using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.UserGuild; | using Model = Discord.API.UserGuild; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Diagnostics; | |||||
| using Discord.Rest; | |||||
| using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Invite; | using Model = Discord.API.Invite; | ||||
| @@ -16,8 +17,8 @@ namespace Discord | |||||
| public override DiscordRestClient Discord { get; } | public override DiscordRestClient Discord { get; } | ||||
| public string Code => Id; | 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) | public Invite(DiscordRestClient discord, Model model) | ||||
| : base(model.Code) | : base(model.Code) | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using Model = Discord.API.InviteMetadata; | using Model = Discord.API.InviteMetadata; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -39,13 +40,10 @@ namespace Discord | |||||
| Channel = channel; | Channel = channel; | ||||
| Author = author; | Author = author; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| if (channel is IGuildChannel) | |||||
| { | |||||
| MentionedUsers = ImmutableArray.Create<IUser>(); | |||||
| MentionedChannelIds = ImmutableArray.Create<ulong>(); | |||||
| MentionedRoles = ImmutableArray.Create<IRole>(); | |||||
| } | |||||
| MentionedUsers = ImmutableArray.Create<IUser>(); | |||||
| MentionedChannelIds = ImmutableArray.Create<ulong>(); | |||||
| MentionedRoles = ImmutableArray.Create<IRole>(); | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| @@ -55,7 +53,6 @@ namespace Discord | |||||
| var guildChannel = Channel as GuildChannel; | var guildChannel = Channel as GuildChannel; | ||||
| var guild = guildChannel?.Guild; | var guild = guildChannel?.Guild; | ||||
| var discord = Discord; | |||||
| if (model.IsTextToSpeech.IsSpecified) | if (model.IsTextToSpeech.IsSpecified) | ||||
| IsTTS = model.IsTextToSpeech.Value; | IsTTS = model.IsTextToSpeech.Value; | ||||
| @@ -1,9 +1,7 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Role; | using Model = Discord.API.Role; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| @@ -7,9 +8,9 @@ namespace Discord | |||||
| { | { | ||||
| internal class SelfUser : User, ISelfUser | 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 string Email { get; private set; } | ||||
| public bool IsVerified { get; private set; } | public bool IsVerified { get; private set; } | ||||
| @@ -43,7 +44,7 @@ namespace Discord | |||||
| { | { | ||||
| if (IsAttached) throw new NotSupportedException(); | 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); | Update(model, UpdateSource.Rest); | ||||
| } | } | ||||
| public async Task ModifyAsync(Action<ModifyCurrentUserParams> func) | public async Task ModifyAsync(Action<ModifyCurrentUserParams> func) | ||||
| @@ -61,27 +62,7 @@ namespace Discord | |||||
| var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); | ||||
| Update(model, UpdateSource.Rest); | Update(model, UpdateSource.Rest); | ||||
| } | } | ||||
| public async Task ModifyStatusAsync(Action<ModifyPresenceParams> 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<ModifyPresenceParams> func) { throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| @@ -0,0 +1,64 @@ | |||||
| using System; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| public partial class DiscordRpcClient | |||||
| { | |||||
| //General | |||||
| 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>>(); | |||||
| //Guild | |||||
| public event Func<Task> GuildUpdated | |||||
| { | |||||
| add { _guildUpdatedEvent.Add(value); } | |||||
| remove { _guildUpdatedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<Task>> _guildUpdatedEvent = new AsyncEvent<Func<Task>>(); | |||||
| //Voice | |||||
| public event Func<Task> VoiceStateUpdated | |||||
| { | |||||
| add { _voiceStateUpdatedEvent.Add(value); } | |||||
| remove { _voiceStateUpdatedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<Task>> _voiceStateUpdatedEvent = new AsyncEvent<Func<Task>>(); | |||||
| //Messages | |||||
| public event Func<ulong, IMessage, Task> MessageReceived | |||||
| { | |||||
| add { _messageReceivedEvent.Add(value); } | |||||
| remove { _messageReceivedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<ulong, IMessage, Task>> _messageReceivedEvent = new AsyncEvent<Func<ulong, IMessage, Task>>(); | |||||
| public event Func<ulong, IMessage, Task> MessageUpdated | |||||
| { | |||||
| add { _messageUpdatedEvent.Add(value); } | |||||
| remove { _messageUpdatedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<ulong, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent<Func<ulong, IMessage, Task>>(); | |||||
| public event Func<ulong, ulong, Task> MessageDeleted | |||||
| { | |||||
| add { _messageDeletedEvent.Add(value); } | |||||
| remove { _messageDeletedEvent.Remove(value); } | |||||
| } | |||||
| private readonly AsyncEvent<Func<ulong, ulong, Task>> _messageDeletedEvent = new AsyncEvent<Func<ulong, ulong, Task>>(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,400 @@ | |||||
| using Discord.API.Rpc; | |||||
| using Discord.Logging; | |||||
| using Discord.Net.Converters; | |||||
| using Discord.Net.Queue; | |||||
| using Discord.Rest; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | |||||
| using System.Runtime.InteropServices; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| public partial class DiscordRpcClient : DiscordRestClient | |||||
| { | |||||
| private readonly ILogger _rpcLogger; | |||||
| private readonly JsonSerializer _serializer; | |||||
| private TaskCompletionSource<bool> _connectTask; | |||||
| private CancellationTokenSource _cancelToken, _reconnectCancelToken; | |||||
| private Task _reconnectTask; | |||||
| private bool _isReconnecting; | |||||
| private bool _canReconnect; | |||||
| public ConnectionState ConnectionState { get; private set; } | |||||
| public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; | |||||
| /// <summary> Creates a new RPC discord client. </summary> | |||||
| public DiscordRpcClient(string clientId, string origin) : this(new DiscordRpcConfig(clientId, origin)) { } | |||||
| /// <summary> Creates a new RPC discord client. </summary> | |||||
| public DiscordRpcClient(DiscordRpcConfig config) | |||||
| : base(config, CreateApiClient(config)) | |||||
| { | |||||
| _rpcLogger = LogManager.CreateLogger("RPC"); | |||||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
| _serializer.Error += (s, e) => | |||||
| { | |||||
| _rpcLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); | |||||
| e.ErrorContext.Handled = true; | |||||
| }; | |||||
| 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 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(); | |||||
| } | |||||
| protected override Task ValidateTokenAsync(TokenType tokenType, string token) | |||||
| { | |||||
| return Task.CompletedTask; //Validation is done in DiscordRpcAPIClient | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public Task ConnectAsync() => ConnectAsync(false); | |||||
| internal async Task ConnectAsync(bool ignoreLoginCheck) | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| _isReconnecting = false; | |||||
| await ConnectInternalAsync(ignoreLoginCheck, false).ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| private async Task ConnectInternalAsync(bool ignoreLoginCheck, bool isReconnecting) | |||||
| { | |||||
| if (!ignoreLoginCheck && LoginState != LoginState.LoggedIn) | |||||
| throw new InvalidOperationException("You must log in before connecting."); | |||||
| if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested) | |||||
| _reconnectCancelToken.Cancel(); | |||||
| var state = ConnectionState; | |||||
| if (state == ConnectionState.Connecting || state == ConnectionState.Connected) | |||||
| await DisconnectInternalAsync(null, isReconnecting).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); | |||||
| await _connectTask.Task.ConfigureAwait(false); | |||||
| _canReconnect = true; | |||||
| ConnectionState = ConnectionState.Connected; | |||||
| await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false); | |||||
| } | |||||
| catch (Exception) | |||||
| { | |||||
| await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false); | |||||
| throw; | |||||
| } | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public async Task DisconnectAsync() | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| _isReconnecting = false; | |||||
| await DisconnectInternalAsync(null, false).ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| 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); | |||||
| 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); | |||||
| ConnectionState = ConnectionState.Disconnected; | |||||
| await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false); | |||||
| await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); | |||||
| } | |||||
| private async Task StartReconnectAsync(Exception ex) | |||||
| { | |||||
| _connectTask?.TrySetException(ex); | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| 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(CancellationToken cancelToken) | |||||
| { | |||||
| try | |||||
| { | |||||
| Random jitter = new Random(); | |||||
| int nextReconnectDelay = 1000; | |||||
| 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 | |||||
| { | |||||
| 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(); } | |||||
| } | |||||
| } | |||||
| catch (OperationCanceledException) | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await _rpcLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false); | |||||
| _reconnectTask = null; | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| } | |||||
| public async Task<string> AuthorizeAsync(string[] scopes, string rpcToken = null) | |||||
| { | |||||
| await ConnectAsync(true).ConfigureAwait(false); | |||||
| var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken).ConfigureAwait(false); | |||||
| await DisconnectAsync().ConfigureAwait(false); | |||||
| 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<string> evnt, Optional<object> payload) | |||||
| { | |||||
| try | |||||
| { | |||||
| switch (cmd) | |||||
| { | |||||
| case "DISPATCH": | |||||
| switch (evnt.Value) | |||||
| { | |||||
| //Connection | |||||
| case "READY": | |||||
| { | |||||
| await _rpcLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); | |||||
| var data = (payload.Value as JToken).ToObject<ReadyEvent>(_serializer); | |||||
| var cancelToken = _cancelToken; | |||||
| var _ = Task.Run(async () => | |||||
| { | |||||
| try | |||||
| { | |||||
| RequestOptions options = new RequestOptions | |||||
| { | |||||
| //CancellationToken = cancelToken //TODO: Implement | |||||
| }; | |||||
| 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); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); | |||||
| return; | |||||
| } | |||||
| }); | |||||
| } | |||||
| break; | |||||
| //Guilds | |||||
| case "GUILD_STATUS": | |||||
| { | |||||
| await _rpcLogger.DebugAsync("Received Dispatch (GUILD_STATUS)").ConfigureAwait(false); | |||||
| await _guildUpdatedEvent.InvokeAsync().ConfigureAwait(false); | |||||
| } | |||||
| break; | |||||
| //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; | |||||
| //Messages | |||||
| case "MESSAGE_CREATE": | |||||
| { | |||||
| await _rpcLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); | |||||
| var data = (payload.Value as JToken).ToObject<MessageEvent>(_serializer); | |||||
| var msg = new RpcMessage(this, 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<MessageEvent>(_serializer); | |||||
| var msg = new RpcMessage(this, 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<MessageEvent>(_serializer); | |||||
| await _messageDeletedEvent.InvokeAsync(data.ChannelId, data.Message.Id).ConfigureAwait(false); | |||||
| } | |||||
| break; | |||||
| //Others | |||||
| default: | |||||
| await _rpcLogger.WarningAsync($"Unknown Dispatch ({evnt})").ConfigureAwait(false); | |||||
| return; | |||||
| } | |||||
| break; | |||||
| /*default: //Other opcodes are used for command responses | |||||
| await _rpcLogger.WarningAsync($"Unknown OpCode ({cmd})").ConfigureAwait(false); | |||||
| return;*/ | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| await _rpcLogger.ErrorAsync($"Error handling {cmd}{(evnt.IsSpecified ? $" ({evnt})" : "")}", ex).ConfigureAwait(false); | |||||
| return; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| using Discord.Net.WebSockets; | |||||
| using Discord.Rest; | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| public class DiscordRpcConfig : DiscordRestConfig | |||||
| { | |||||
| public const int RpcAPIVersion = 1; | |||||
| public const int PortRangeStart = 6463; | |||||
| public const int PortRangeEnd = 6472; | |||||
| public DiscordRpcConfig(string clientId, string origin) | |||||
| { | |||||
| ClientId = clientId; | |||||
| Origin = origin; | |||||
| } | |||||
| /// <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 origin used for this RPC connection. </summary> | |||||
| public string Origin { get; set; } | |||||
| /// <summary> Gets or sets the provider used to generate new websocket connections. </summary> | |||||
| public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| /*public interface IRemoteUserGuild : ISnowflakeEntity | |||||
| { | |||||
| /// <summary> Gets the name of this guild. </summary> | |||||
| string Name { get; } | |||||
| }*/ | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using Model = Discord.API.Rpc.RpcUserGuild; | |||||
| namespace Discord.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<ulong>.IsAttached => false; | |||||
| }*/ | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| public enum RpcChannelEvent | |||||
| { | |||||
| VoiceStateCreate, | |||||
| VoiceStateUpdate, | |||||
| VoiceStateDelete, | |||||
| SpeakingStart, | |||||
| SpeakingStop, | |||||
| MessageCreate, | |||||
| MessageUpdate, | |||||
| MessageDelete | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| namespace Discord.Rpc | |||||
| { | |||||
| public enum RpcGuildEvent | |||||
| { | |||||
| GuildStatus | |||||
| } | |||||
| } | |||||
| @@ -1,7 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | |||||
| namespace Discord.WebSocket | |||||
| { | { | ||||
| //TODO: Add event docstrings | //TODO: Add event docstrings | ||||
| public partial class DiscordSocketClient | public partial class DiscordSocketClient | ||||
| @@ -1,9 +1,10 @@ | |||||
| using Discord.API.Gateway; | using Discord.API.Gateway; | ||||
| using Discord.Audio; | using Discord.Audio; | ||||
| using Discord.Extensions; | |||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
| using Discord.Net.Queue; | |||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | |||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
| using System; | using System; | ||||
| @@ -14,7 +15,7 @@ using System.Linq; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | |||||
| namespace Discord.WebSocket | |||||
| { | { | ||||
| public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient | public partial class DiscordSocketClient : DiscordRestClient, IDiscordClient | ||||
| { | { | ||||
| @@ -52,6 +53,7 @@ namespace Discord | |||||
| internal DataStore DataStore { get; private set; } | internal DataStore DataStore { get; private set; } | ||||
| internal WebSocketProvider WebSocketProvider { 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 SocketSelfUser CurrentUser => _currentUser as SocketSelfUser; | ||||
| internal IReadOnlyCollection<SocketGuild> Guilds => DataStore.Guilds; | internal IReadOnlyCollection<SocketGuild> Guilds => DataStore.Guilds; | ||||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | ||||
| @@ -60,7 +62,7 @@ namespace Discord | |||||
| public DiscordSocketClient() : this(new DiscordSocketConfig()) { } | public DiscordSocketClient() : this(new DiscordSocketConfig()) { } | ||||
| /// <summary> Creates a new REST/WebSocket discord client. </summary> | /// <summary> Creates a new REST/WebSocket discord client. </summary> | ||||
| public DiscordSocketClient(DiscordSocketConfig config) | public DiscordSocketClient(DiscordSocketConfig config) | ||||
| : base(config) | |||||
| : base(config, CreateApiClient(config)) | |||||
| { | { | ||||
| ShardId = config.ShardId; | ShardId = config.ShardId; | ||||
| TotalShards = config.TotalShards; | TotalShards = config.TotalShards; | ||||
| @@ -106,8 +108,10 @@ namespace Discord | |||||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | ||||
| _largeGuilds = new ConcurrentQueue<ulong>(); | _largeGuilds = new ConcurrentQueue<ulong>(); | ||||
| } | } | ||||
| 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); | var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); | ||||
| _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); | _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); | ||||
| @@ -162,7 +166,7 @@ namespace Discord | |||||
| if (_sessionId != null) | if (_sessionId != null) | ||||
| await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | ||||
| else | else | ||||
| await ApiClient.SendIdentifyAsync().ConfigureAwait(false); | |||||
| await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards:TotalShards).ConfigureAwait(false); | |||||
| await _connectTask.Task.ConfigureAwait(false); | await _connectTask.Task.ConfigureAwait(false); | ||||
| _canReconnect = true; | _canReconnect = true; | ||||
| @@ -530,7 +534,7 @@ namespace Discord | |||||
| _sessionId = null; | _sessionId = null; | ||||
| _lastSeq = 0; | _lastSeq = 0; | ||||
| await ApiClient.SendIdentifyAsync().ConfigureAwait(false); | |||||
| await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); | |||||
| } | } | ||||
| break; | break; | ||||
| case GatewayOpCode.Reconnect: | case GatewayOpCode.Reconnect: | ||||
| @@ -1467,6 +1471,9 @@ namespace Discord | |||||
| return; | return; | ||||
| //Ignored (User only) | //Ignored (User only) | ||||
| case "CHANNEL_PINS_UPDATE": | |||||
| await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_UPDATE)"); | |||||
| break; | |||||
| case "USER_SETTINGS_UPDATE": | case "USER_SETTINGS_UPDATE": | ||||
| await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); | ||||
| return; | return; | ||||
| @@ -1,5 +1,6 @@ | |||||
| using Discord.Audio; | using Discord.Audio; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -14,11 +15,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> | /// <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; | 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> | /// <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. | /// 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. | /// Decreasing this may reduce CPU usage while increasing login time and network usage. | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.Extensions; | |||||
| using Discord.Rest; | |||||
| using Discord.WebSocket; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -51,7 +52,7 @@ namespace Discord | |||||
| return result; | return result; | ||||
| return null; | return null; | ||||
| } | } | ||||
| public override IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) | |||||
| public override IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| { | { | ||||
| if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | ||||
| if (limit == 0) return ImmutableArray<SocketMessage>.Empty; | if (limit == 0) return ImmutableArray<SocketMessage>.Empty; | ||||
| @@ -1,4 +1,6 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Rest; | |||||
| using Discord.WebSocket; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -12,7 +14,7 @@ namespace Discord | |||||
| private readonly DiscordSocketClient _discord; | private readonly DiscordSocketClient _discord; | ||||
| private readonly ISocketMessageChannel _channel; | private readonly ISocketMessageChannel _channel; | ||||
| public virtual IReadOnlyCollection<SocketMessage> Messages | |||||
| public virtual IReadOnlyCollection<SocketMessage> Messages | |||||
| => ImmutableArray.Create<SocketMessage>(); | => ImmutableArray.Create<SocketMessage>(); | ||||
| public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) | public MessageManager(DiscordSocketClient discord, ISocketMessageChannel channel) | ||||
| @@ -25,7 +27,7 @@ namespace Discord | |||||
| public virtual SocketMessage Remove(ulong id) => null; | public virtual SocketMessage Remove(ulong id) => null; | ||||
| public virtual SocketMessage Get(ulong id) => null; | public virtual SocketMessage Get(ulong id) => null; | ||||
| public virtual IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) | |||||
| public virtual IImmutableList<SocketMessage> GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| => ImmutableArray.Create<SocketMessage>(); | => ImmutableArray.Create<SocketMessage>(); | ||||
| public virtual async Task<SocketMessage> DownloadAsync(ulong id) | public virtual async Task<SocketMessage> DownloadAsync(ulong id) | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Collections.Generic; | |||||
| using Discord.WebSocket; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using MessageModel = Discord.API.Message; | using MessageModel = Discord.API.Message; | ||||
| @@ -1,4 +1,4 @@ | |||||
| using Discord.Extensions; | |||||
| using Discord.WebSocket; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -1,4 +1,6 @@ | |||||
| using System.Collections.Generic; | |||||
| using Discord.Rest; | |||||
| using Discord.WebSocket; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -48,11 +50,11 @@ namespace Discord | |||||
| { | { | ||||
| return await _messages.DownloadAsync(id).ConfigureAwait(false); | return await _messages.DownloadAsync(id).ConfigureAwait(false); | ||||
| } | } | ||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordRestConfig.MaxMessagesPerBatch) | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| { | { | ||||
| return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); | return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); | ||||
| } | } | ||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordRestConfig.MaxMessagesPerBatch) | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| { | { | ||||
| return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); | return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Discord.Audio; | using Discord.Audio; | ||||
| using Discord.WebSocket; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| @@ -1,5 +1,5 @@ | |||||
| using Discord.Audio; | using Discord.Audio; | ||||
| using Discord.Extensions; | |||||
| using Discord.WebSocket; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using Model = Discord.API.Message; | |||||
| using Discord.WebSocket; | |||||
| using Model = Discord.API.Message; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using Discord.WebSocket; | |||||
| using System; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using PresenceModel = Discord.API.Presence; | using PresenceModel = Discord.API.Presence; | ||||