From b93abcc95ba86a08d897da38f26b778cc7165a3a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 27 May 2016 01:52:11 -0300 Subject: [PATCH] Added initial websocket support --- src/Discord.Net/API/DiscordAPIClient.cs | 215 ++++- src/Discord.Net/API/DiscordAPISocketClient.cs | 11 + .../API/Gateway/GuildMembersChunkEvent.cs | 12 + .../API/Gateway/GuildRoleCreateEvent.cs | 12 + .../API/Gateway/GuildRoleUpdateEvent.cs | 12 + src/Discord.Net/API/Gateway/IdentifyParams.cs | 17 + src/Discord.Net/API/Gateway/OpCodes.cs | 24 + src/Discord.Net/API/Gateway/ReadyEvent.cs | 40 + .../API/Gateway/RequestMembersParams.cs | 14 + src/Discord.Net/API/Gateway/ResumeParams.cs | 12 + src/Discord.Net/API/Gateway/ResumedEvent.cs | 10 + .../API/Gateway/TypingStartEvent.cs | 14 + .../API/Gateway/UpdateStatusParams.cs | 12 + .../API/Gateway/UpdateVoiceParams.cs | 16 + .../API/Gateway/VoiceServerUpdateEvent.cs | 14 + src/Discord.Net/API/IWebSocketMessage.cs | 9 - src/Discord.Net/API/WebSocketMessage.cs | 10 +- src/Discord.Net/Common/EventExtensions.cs | 13 - src/Discord.Net/ConnectionState.cs | 10 + src/Discord.Net/{Common => }/DateTimeUtils.cs | 3 +- .../Entities/Channels/ChannelType.cs | 0 .../Entities/Channels/IChannel.cs | 0 .../Entities/Channels/IDMChannel.cs | 0 .../Entities/Channels/IGuildChannel.cs | 0 .../Entities/Channels/IMessageChannel.cs | 0 .../Entities/Channels/ITextChannel.cs | 0 .../Entities/Channels/IVoiceChannel.cs | 0 .../{Common => }/Entities/Guilds/Emoji.cs | 0 .../{Common => }/Entities/Guilds/IGuild.cs | 0 .../Entities/Guilds/IGuildEmbed.cs | 0 .../Entities/Guilds/IGuildIntegration.cs | 0 .../Entities/Guilds/IUserGuild.cs | 0 .../Entities/Guilds/IVoiceRegion.cs | 0 .../Entities/Guilds/IntegrationAccount.cs | 0 .../Entities/Guilds/VoiceRegion.cs | 0 .../{Common => }/Entities/IDeletable.cs | 0 .../{Common => }/Entities/IEntity.cs | 0 .../{Common => }/Entities/IMentionable.cs | 0 .../{Common => }/Entities/ISnowflakeEntity.cs | 0 .../{Common => }/Entities/IUpdateable.cs | 0 .../{Common => }/Entities/Invites/IInvite.cs | 0 .../Entities/Invites/IInviteMetadata.cs | 0 .../{Common => }/Entities/Invites/Invite.cs | 0 .../Entities/Invites/InviteMetadata.cs | 0 .../Entities/Messages/Attachment.cs | 0 .../Entities/Messages/Direction.cs | 0 .../{Common => }/Entities/Messages/Embed.cs | 0 .../Entities/Messages/EmbedProvider.cs | 0 .../Entities/Messages/EmbedThumbnail.cs | 0 .../Entities/Messages/IMessage.cs | 0 .../Entities/Permissions/ChannelPermission.cs | 0 .../Permissions/ChannelPermissions.cs | 0 .../Entities/Permissions/GuildPermission.cs | 0 .../Entities/Permissions/GuildPermissions.cs | 0 .../Entities/Permissions/Overwrite.cs | 0 .../Permissions/OverwritePermissions.cs | 0 .../Entities/Permissions/PermValue.cs | 0 .../Entities/Permissions/PermissionTarget.cs | 0 .../Entities/Permissions/Permissions.cs | 0 .../{Common => }/Entities/Roles/Color.cs | 0 .../{Common => }/Entities/Roles/IRole.cs | 0 .../{Common => }/Entities/Users/Connection.cs | 0 .../{Common => }/Entities/Users/Game.cs | 0 .../Entities/Users/IConnection.cs | 0 .../{Common => }/Entities/Users/IGuildUser.cs | 0 .../{Common => }/Entities/Users/ISelfUser.cs | 0 .../{Common => }/Entities/Users/IUser.cs | 0 .../Entities/Users/IVoiceState.cs.old | 0 .../{Common => }/Entities/Users/StreamType.cs | 0 .../{Common => }/Entities/Users/UserStatus.cs | 0 src/Discord.Net/EventExtensions.cs | 45 + .../Events/LogMessageEventArgs.cs | 0 .../Events/SentRequestEventArgs.cs | 0 src/Discord.Net/IDiscordClient.cs | 12 +- src/Discord.Net/Logging/ILogger.cs | 37 +- src/Discord.Net/Logging/LogManager.cs | 87 +- src/Discord.Net/LoginState.cs | 10 + src/Discord.Net/{Common => }/MentionUtils.cs | 0 .../RequestQueue => Queue}/BucketGroup.cs | 2 +- src/Discord.Net/Net/Queue/GlobalBucket.cs | 12 + .../RequestQueue => Queue}/GuildBucket.cs | 2 +- src/Discord.Net/Net/Queue/IQueuedRequest.cs | 13 + .../RequestQueue => Queue}/IRequestQueue.cs | 2 +- .../RequestQueue => Queue}/RequestQueue.cs | 17 +- .../RequestQueueBucket.cs | 31 +- src/Discord.Net/Net/Queue/RestRequest.cs | 53 ++ src/Discord.Net/Net/Queue/WebSocketRequest.cs | 34 + src/Discord.Net/Net/Rest/DefaultRestClient.cs | 29 +- src/Discord.Net/Net/Rest/IRestClient.cs | 6 +- .../Net/Rest/RequestQueue/GlobalBucket.cs | 9 - .../Net/Rest/RequestQueue/RestRequest.cs | 42 - .../Net/WebSockets/DefaultWebsocketClient.cs | 183 ++-- .../Net/WebSockets/IWebSocketClient.cs | 10 +- .../Net/WebSockets/WebSocketProvider.cs | 2 +- src/Discord.Net/Rest/DiscordClient.cs | 119 ++- .../WebSocket/Data/DataStoreProvider.cs | 4 + .../WebSocket/Data/DefaultDataStore.cs | 110 +++ src/Discord.Net/WebSocket/Data/IDataStore.cs | 28 + .../WebSocket/Data/SharedDataStore.cs | 7 + src/Discord.Net/WebSocket/DiscordClient.cs | 863 +++++++++++++++--- .../WebSocket/DiscordSocketConfig.cs | 7 +- .../WebSocket/Entities/Users/User.cs | 3 +- .../WebSocket/Events/ChannelEventArgs.cs | 14 - .../Events/ChannelUpdatedEventArgs.cs | 14 - .../WebSocket/Events/CurrentUserEventArgs.cs | 14 - .../Events/CurrentUserUpdatedEventArgs.cs | 14 - .../WebSocket/Events/DisconnectedEventArgs.cs | 16 - .../WebSocket/Events/GuildEventArgs.cs | 14 - .../WebSocket/Events/GuildUpdatedEventArgs.cs | 14 - .../WebSocket/Events/MessageEventArgs.cs | 14 - .../Events/MessageUpdatedEventArgs.cs | 14 - .../WebSocket/Events/RoleEventArgs.cs | 14 - .../WebSocket/Events/RoleUpdatedEventArgs.cs | 14 - .../WebSocket/Events/TypingEventArgs.cs | 16 - .../WebSocket/Events/UserEventArgs.cs | 14 - .../WebSocket/Events/UserUpdatedEventArgs.cs | 14 - .../WebSocket/Events/VoiceChannelEventArgs.cs | 14 - src/Discord.Net/project.json | 4 +- 118 files changed, 1713 insertions(+), 764 deletions(-) create mode 100644 src/Discord.Net/API/DiscordAPISocketClient.cs create mode 100644 src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs create mode 100644 src/Discord.Net/API/Gateway/IdentifyParams.cs create mode 100644 src/Discord.Net/API/Gateway/OpCodes.cs create mode 100644 src/Discord.Net/API/Gateway/ReadyEvent.cs create mode 100644 src/Discord.Net/API/Gateway/RequestMembersParams.cs create mode 100644 src/Discord.Net/API/Gateway/ResumeParams.cs create mode 100644 src/Discord.Net/API/Gateway/ResumedEvent.cs create mode 100644 src/Discord.Net/API/Gateway/TypingStartEvent.cs create mode 100644 src/Discord.Net/API/Gateway/UpdateStatusParams.cs create mode 100644 src/Discord.Net/API/Gateway/UpdateVoiceParams.cs create mode 100644 src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs delete mode 100644 src/Discord.Net/API/IWebSocketMessage.cs delete mode 100644 src/Discord.Net/Common/EventExtensions.cs create mode 100644 src/Discord.Net/ConnectionState.cs rename src/Discord.Net/{Common => }/DateTimeUtils.cs (79%) rename src/Discord.Net/{Common => }/Entities/Channels/ChannelType.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/IChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/IDMChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/IGuildChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/IMessageChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/ITextChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Channels/IVoiceChannel.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/Emoji.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IGuild.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IGuildEmbed.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IGuildIntegration.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IUserGuild.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IVoiceRegion.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/IntegrationAccount.cs (100%) rename src/Discord.Net/{Common => }/Entities/Guilds/VoiceRegion.cs (100%) rename src/Discord.Net/{Common => }/Entities/IDeletable.cs (100%) rename src/Discord.Net/{Common => }/Entities/IEntity.cs (100%) rename src/Discord.Net/{Common => }/Entities/IMentionable.cs (100%) rename src/Discord.Net/{Common => }/Entities/ISnowflakeEntity.cs (100%) rename src/Discord.Net/{Common => }/Entities/IUpdateable.cs (100%) rename src/Discord.Net/{Common => }/Entities/Invites/IInvite.cs (100%) rename src/Discord.Net/{Common => }/Entities/Invites/IInviteMetadata.cs (100%) rename src/Discord.Net/{Common => }/Entities/Invites/Invite.cs (100%) rename src/Discord.Net/{Common => }/Entities/Invites/InviteMetadata.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/Attachment.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/Direction.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/Embed.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/EmbedProvider.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/EmbedThumbnail.cs (100%) rename src/Discord.Net/{Common => }/Entities/Messages/IMessage.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/ChannelPermission.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/ChannelPermissions.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/GuildPermission.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/GuildPermissions.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/Overwrite.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/OverwritePermissions.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/PermValue.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/PermissionTarget.cs (100%) rename src/Discord.Net/{Common => }/Entities/Permissions/Permissions.cs (100%) rename src/Discord.Net/{Common => }/Entities/Roles/Color.cs (100%) rename src/Discord.Net/{Common => }/Entities/Roles/IRole.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/Connection.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/Game.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/IConnection.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/IGuildUser.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/ISelfUser.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/IUser.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/IVoiceState.cs.old (100%) rename src/Discord.Net/{Common => }/Entities/Users/StreamType.cs (100%) rename src/Discord.Net/{Common => }/Entities/Users/UserStatus.cs (100%) create mode 100644 src/Discord.Net/EventExtensions.cs rename src/Discord.Net/{Common => }/Events/LogMessageEventArgs.cs (100%) rename src/Discord.Net/{Common => }/Events/SentRequestEventArgs.cs (100%) create mode 100644 src/Discord.Net/LoginState.cs rename src/Discord.Net/{Common => }/MentionUtils.cs (100%) rename src/Discord.Net/Net/{Rest/RequestQueue => Queue}/BucketGroup.cs (71%) create mode 100644 src/Discord.Net/Net/Queue/GlobalBucket.cs rename src/Discord.Net/Net/{Rest/RequestQueue => Queue}/GuildBucket.cs (83%) create mode 100644 src/Discord.Net/Net/Queue/IQueuedRequest.cs rename src/Discord.Net/Net/{Rest/RequestQueue => Queue}/IRequestQueue.cs (87%) rename src/Discord.Net/Net/{Rest/RequestQueue => Queue}/RequestQueue.cs (92%) rename src/Discord.Net/Net/{Rest/RequestQueue => Queue}/RequestQueueBucket.cs (86%) create mode 100644 src/Discord.Net/Net/Queue/RestRequest.cs create mode 100644 src/Discord.Net/Net/Queue/WebSocketRequest.cs delete mode 100644 src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs delete mode 100644 src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs create mode 100644 src/Discord.Net/WebSocket/Data/DataStoreProvider.cs create mode 100644 src/Discord.Net/WebSocket/Data/DefaultDataStore.cs create mode 100644 src/Discord.Net/WebSocket/Data/IDataStore.cs create mode 100644 src/Discord.Net/WebSocket/Data/SharedDataStore.cs delete mode 100644 src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/GuildEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/MessageEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/RoleEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/TypingEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/UserEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs delete mode 100644 src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index edfdb9a64..2f285030d 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -1,7 +1,9 @@ using Discord.API.Rest; using Discord.Net; using Discord.Net.Converters; +using Discord.Net.Queue; using Discord.Net.Rest; +using Discord.Net.WebSockets; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -17,77 +19,202 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordApiClient + public class DiscordApiClient : IDisposable { - internal event EventHandler SentRequest; + internal event Func SentRequest; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; private readonly IRestClient _restClient; - private CancellationToken _cancelToken; + private readonly IWebSocketClient _gatewayClient; + private readonly SemaphoreSlim _connectionLock; + private CancellationTokenSource _loginCancelToken, _connectCancelToken; + private bool _isDisposed; + public LoginState LoginState { get; private set; } + public ConnectionState ConnectionState { get; private set; } public TokenType AuthTokenType { get; private set; } - public IRestClient RestClient { get; private set; } - public IRequestQueue RequestQueue { get; private set; } - public DiscordApiClient(RestClientProvider restClientProvider) + public DiscordApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider = null, JsonSerializer serializer = null, RequestQueue requestQueue = null) { + _connectionLock = new SemaphoreSlim(1, 1); + + _requestQueue = requestQueue ?? new RequestQueue(); + _restClient = restClientProvider(DiscordConfig.ClientAPIUrl); _restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); + if (webSocketProvider != null) + { + _gatewayClient = webSocketProvider(); + _gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); + } - _requestQueue = new RequestQueue(_restClient); - - _serializer = new JsonSerializer() + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + void Dispose(bool disposing) + { + if (!_isDisposed) { - ContractResolver = new DiscordContractResolver() - }; + if (disposing) + { + _loginCancelToken?.Dispose(); + _connectCancelToken?.Dispose(); + } + _isDisposed = true; + } } + public void Dispose() => Dispose(true); - public async Task Login(TokenType tokenType, string token, CancellationToken cancelToken) + public async Task Login(LoginParams args) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternal(TokenType.User, null, args, true).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + public async Task Login(TokenType tokenType, string token) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternal(tokenType, token, null, false).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternal(TokenType tokenType, string token, LoginParams args, bool doLogin) { - AuthTokenType = tokenType; - _cancelToken = cancelToken; - await _requestQueue.SetCancelToken(cancelToken).ConfigureAwait(false); + if (LoginState != LoginState.LoggedOut) + await LogoutInternal().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; - switch (tokenType) + try { - 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)); + _loginCancelToken = new CancellationTokenSource(); + + AuthTokenType = TokenType.User; + _restClient.SetHeader("authorization", null); + await _requestQueue.SetCancelToken(_loginCancelToken.Token).ConfigureAwait(false); + _restClient.SetCancelToken(_loginCancelToken.Token); + + if (doLogin) + { + var response = await Send("POST", "auth/login", args, GlobalBucket.Login).ConfigureAwait(false); + token = response.Token; + } + + AuthTokenType = tokenType; + 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); + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternal().ConfigureAwait(false); + throw; + } + } + + public async Task Logout() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternal().ConfigureAwait(false); } + finally { _connectionLock.Release(); } + } + private async Task LogoutInternal() + { + //TODO: An exception here will lock the client into the unusable LoggingOut state. How should we handle? (Add same solution to both DiscordClients too) + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + try { _loginCancelToken?.Cancel(false); } + catch { } + + await DisconnectInternal().ConfigureAwait(false); + await _requestQueue.Clear().ConfigureAwait(false); - _restClient.SetHeader("authorization", token); + await _requestQueue.SetCancelToken(CancellationToken.None).ConfigureAwait(false); + _restClient.SetCancelToken(CancellationToken.None); + + LoginState = LoginState.LoggedOut; + } + + public async Task Connect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } } - public async Task Login(LoginParams args, CancellationToken cancelToken) + private async Task ConnectInternal() { - AuthTokenType = TokenType.User; - _restClient.SetHeader("authorization", null); - _cancelToken = cancelToken; + 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."); - LoginResponse response; + ConnectionState = ConnectionState.Connecting; try { - response = await Send("POST", "auth/login", args, GlobalBucket.Login).ConfigureAwait(false); + _connectCancelToken = new CancellationTokenSource(); + if (_gatewayClient != null) + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + + var gatewayResponse = await GetGateway().ConfigureAwait(false); + await _gatewayClient.Connect(gatewayResponse.Url).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; } - catch + catch (Exception) { - _cancelToken = CancellationToken.None; + await DisconnectInternal().ConfigureAwait(false); throw; } + } - _restClient.SetHeader("authorization", response.Token); + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } } - public async Task Logout() + private async Task DisconnectInternal() { - await _requestQueue.Clear().ConfigureAwait(false); + 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.Disconnect().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; } //Core @@ -134,32 +261,28 @@ namespace Discord.API private async Task SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) { - _cancelToken.ThrowIfCancellationRequested(); - var stopwatch = Stopwatch.StartNew(); string json = null; if (payload != null) json = Serialize(payload); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + await SentRequest.Raise(new SentRequestEventArgs(method, endpoint, bytes, milliseconds)).ConfigureAwait(false); return responseStream; } private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) { - _cancelToken.ThrowIfCancellationRequested(); - var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + await SentRequest.Raise(new SentRequestEventArgs(method, endpoint, bytes, milliseconds)).ConfigureAwait(false); return responseStream; } diff --git a/src/Discord.Net/API/DiscordAPISocketClient.cs b/src/Discord.Net/API/DiscordAPISocketClient.cs new file mode 100644 index 000000000..fcff89923 --- /dev/null +++ b/src/Discord.Net/API/DiscordAPISocketClient.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class DiscordAPISocketClient + { + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs b/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs new file mode 100644 index 000000000..d6402731a --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMembersChunkEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs new file mode 100644 index 000000000..f05543bf6 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildRoleCreateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs new file mode 100644 index 000000000..345154432 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildRoleUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net/API/Gateway/IdentifyParams.cs new file mode 100644 index 000000000..96c45927a --- /dev/null +++ b/src/Discord.Net/API/Gateway/IdentifyParams.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + public class IdentifyCommand + { + [JsonProperty("token")] + public string Token { get; set; } + [JsonProperty("properties")] + public IDictionary Properties { get; set; } + [JsonProperty("large_threshold")] + public int LargeThreshold { get; set; } + [JsonProperty("compress")] + public bool UseCompression { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/OpCodes.cs b/src/Discord.Net/API/Gateway/OpCodes.cs new file mode 100644 index 000000000..cf237bd03 --- /dev/null +++ b/src/Discord.Net/API/Gateway/OpCodes.cs @@ -0,0 +1,24 @@ +namespace Discord.API.Gateway +{ + public enum OpCodes : byte + { + /// C←S - Used to send most events. + Dispatch = 0, + /// C↔S - Used to keep the connection alive and measure latency. + Heartbeat = 1, + /// C→S - Used to associate a connection with a token and specify configuration. + Identify = 2, + /// C→S - Used to update client's status and current game id. + StatusUpdate = 3, + /// C→S - Used to join a particular voice channel. + VoiceStateUpdate = 4, + /// C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. + VoiceServerPing = 5, + /// C→S - Used to resume a connection after a redirect occurs. + Resume = 6, + /// C←S - Used to notify a client that they must reconnect to another gateway. + Reconnect = 7, + /// C→S - Used to request all members that were withheld by large_threshold + RequestGuildMembers = 8 + } +} diff --git a/src/Discord.Net/API/Gateway/ReadyEvent.cs b/src/Discord.Net/API/Gateway/ReadyEvent.cs new file mode 100644 index 000000000..c0384c100 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ReadyEvent.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ReadyEvent + { + public class ReadState + { + [JsonProperty("id")] + public string ChannelId { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public string LastMessageId { get; set; } + } + + [JsonProperty("v")] + public int Version { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("read_state")] + public ReadState[] ReadStates { get; set; } + [JsonProperty("guilds")] + public Guild[] Guilds { get; set; } + [JsonProperty("private_channels")] + public Channel[] PrivateChannels { get; set; } + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + + //Ignored + [JsonProperty("user_settings")] + public object UserSettings { get; set; } + [JsonProperty("user_guild_settings")] + public object UserGuildSettings { get; set; } + [JsonProperty("tutorial")] + public object Tutorial { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs new file mode 100644 index 000000000..16077939d --- /dev/null +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class RequestMembersCommand + { + [JsonProperty("guild_id")] + public ulong[] GuildId { get; set; } + [JsonProperty("query")] + public string Query { get; set; } + [JsonProperty("limit")] + public int Limit { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ResumeParams.cs b/src/Discord.Net/API/Gateway/ResumeParams.cs new file mode 100644 index 000000000..c0fb296f4 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ResumeParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ResumeCommand + { + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("seq")] + public uint Sequence { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ResumedEvent.cs b/src/Discord.Net/API/Gateway/ResumedEvent.cs new file mode 100644 index 000000000..6087a0c38 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ResumedEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ResumedEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/TypingStartEvent.cs b/src/Discord.Net/API/Gateway/TypingStartEvent.cs new file mode 100644 index 000000000..2e3829bc7 --- /dev/null +++ b/src/Discord.Net/API/Gateway/TypingStartEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class TypingStartEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("timestamp")] + public int Timestamp { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/UpdateStatusParams.cs b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs new file mode 100644 index 000000000..e673cb51e --- /dev/null +++ b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class UpdateStatusCommand + { + [JsonProperty("idle_since")] + public long? IdleSince { get; set; } + [JsonProperty("game")] + public Game Game { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs new file mode 100644 index 000000000..689394e5d --- /dev/null +++ b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class UpdateVoiceCommand + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + [JsonProperty("self_mute")] + public bool IsSelfMuted { get; set; } + [JsonProperty("self_deaf")] + public bool IsSelfDeafened { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs b/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs new file mode 100644 index 000000000..036d535c2 --- /dev/null +++ b/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class VoiceServerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("endpoint")] + public string Endpoint { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/IWebSocketMessage.cs b/src/Discord.Net/API/IWebSocketMessage.cs deleted file mode 100644 index 526f3119f..000000000 --- a/src/Discord.Net/API/IWebSocketMessage.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.API -{ - public interface IWebSocketMessage - { - int OpCode { get; } - object Payload { get; } - bool IsPrivate { get; } - } -} diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net/API/WebSocketMessage.cs index 05d5e779d..285f5e13f 100644 --- a/src/Discord.Net/API/WebSocketMessage.cs +++ b/src/Discord.Net/API/WebSocketMessage.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Discord.API { @@ -11,13 +12,6 @@ namespace Discord.API [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] public uint? Sequence { get; set; } [JsonProperty("d")] - public object Payload { get; set; } - - public WebSocketMessage() { } - public WebSocketMessage(IWebSocketMessage msg) - { - Operation = msg.OpCode; - Payload = msg.Payload; - } + public JToken Payload { get; set; } } } diff --git a/src/Discord.Net/Common/EventExtensions.cs b/src/Discord.Net/Common/EventExtensions.cs deleted file mode 100644 index a024822b7..000000000 --- a/src/Discord.Net/Common/EventExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord -{ - internal static class EventExtensions - { - public static void Raise(this EventHandler eventHandler, object sender) - => eventHandler?.Invoke(sender, EventArgs.Empty); - public static void Raise(this EventHandler eventHandler, object sender, T eventArgs) - where T : EventArgs - => eventHandler?.Invoke(sender, eventArgs); - } -} diff --git a/src/Discord.Net/ConnectionState.cs b/src/Discord.Net/ConnectionState.cs new file mode 100644 index 000000000..42c505ccd --- /dev/null +++ b/src/Discord.Net/ConnectionState.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum ConnectionState : byte + { + Disconnected, + Connecting, + Connected, + Disconnecting + } +} diff --git a/src/Discord.Net/Common/DateTimeUtils.cs b/src/Discord.Net/DateTimeUtils.cs similarity index 79% rename from src/Discord.Net/Common/DateTimeUtils.cs rename to src/Discord.Net/DateTimeUtils.cs index 57db38134..92a42e74b 100644 --- a/src/Discord.Net/Common/DateTimeUtils.cs +++ b/src/Discord.Net/DateTimeUtils.cs @@ -5,6 +5,7 @@ namespace Discord internal static class DateTimeUtils { private const ulong EpochTicks = 621355968000000000UL; + private const ulong DiscordEpochMillis = 1420070400000UL; public static DateTime FromEpochMilliseconds(ulong value) => new DateTime((long)(value * TimeSpan.TicksPerMillisecond + EpochTicks), DateTimeKind.Utc); @@ -12,6 +13,6 @@ namespace Discord => new DateTime((long)(value * TimeSpan.TicksPerSecond + EpochTicks), DateTimeKind.Utc); public static DateTime FromSnowflake(ulong value) - => FromEpochMilliseconds((value >> 22) + 1420070400000UL); + => FromEpochMilliseconds((value >> 22) + DiscordEpochMillis); } } diff --git a/src/Discord.Net/Common/Entities/Channels/ChannelType.cs b/src/Discord.Net/Entities/Channels/ChannelType.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/ChannelType.cs rename to src/Discord.Net/Entities/Channels/ChannelType.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/IChannel.cs rename to src/Discord.Net/Entities/Channels/IChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Entities/Channels/IDMChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/IDMChannel.cs rename to src/Discord.Net/Entities/Channels/IDMChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IGuildChannel.cs b/src/Discord.Net/Entities/Channels/IGuildChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/IGuildChannel.cs rename to src/Discord.Net/Entities/Channels/IGuildChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/IMessageChannel.cs rename to src/Discord.Net/Entities/Channels/IMessageChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/ITextChannel.cs b/src/Discord.Net/Entities/Channels/ITextChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/ITextChannel.cs rename to src/Discord.Net/Entities/Channels/ITextChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/IVoiceChannel.cs rename to src/Discord.Net/Entities/Channels/IVoiceChannel.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/Emoji.cs b/src/Discord.Net/Entities/Guilds/Emoji.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/Emoji.cs rename to src/Discord.Net/Entities/Guilds/Emoji.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IGuild.cs rename to src/Discord.Net/Entities/Guilds/IGuild.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuildEmbed.cs b/src/Discord.Net/Entities/Guilds/IGuildEmbed.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IGuildEmbed.cs rename to src/Discord.Net/Entities/Guilds/IGuildEmbed.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs rename to src/Discord.Net/Entities/Guilds/IGuildIntegration.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IUserGuild.cs b/src/Discord.Net/Entities/Guilds/IUserGuild.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IUserGuild.cs rename to src/Discord.Net/Entities/Guilds/IUserGuild.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IVoiceRegion.cs rename to src/Discord.Net/Entities/Guilds/IVoiceRegion.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IntegrationAccount.cs rename to src/Discord.Net/Entities/Guilds/IntegrationAccount.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Entities/Guilds/VoiceRegion.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/VoiceRegion.cs rename to src/Discord.Net/Entities/Guilds/VoiceRegion.cs diff --git a/src/Discord.Net/Common/Entities/IDeletable.cs b/src/Discord.Net/Entities/IDeletable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IDeletable.cs rename to src/Discord.Net/Entities/IDeletable.cs diff --git a/src/Discord.Net/Common/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IEntity.cs rename to src/Discord.Net/Entities/IEntity.cs diff --git a/src/Discord.Net/Common/Entities/IMentionable.cs b/src/Discord.Net/Entities/IMentionable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IMentionable.cs rename to src/Discord.Net/Entities/IMentionable.cs diff --git a/src/Discord.Net/Common/Entities/ISnowflakeEntity.cs b/src/Discord.Net/Entities/ISnowflakeEntity.cs similarity index 100% rename from src/Discord.Net/Common/Entities/ISnowflakeEntity.cs rename to src/Discord.Net/Entities/ISnowflakeEntity.cs diff --git a/src/Discord.Net/Common/Entities/IUpdateable.cs b/src/Discord.Net/Entities/IUpdateable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IUpdateable.cs rename to src/Discord.Net/Entities/IUpdateable.cs diff --git a/src/Discord.Net/Common/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Invites/IInvite.cs rename to src/Discord.Net/Entities/Invites/IInvite.cs diff --git a/src/Discord.Net/Common/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Invites/IInviteMetadata.cs rename to src/Discord.Net/Entities/Invites/IInviteMetadata.cs diff --git a/src/Discord.Net/Common/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Invites/Invite.cs rename to src/Discord.Net/Entities/Invites/Invite.cs diff --git a/src/Discord.Net/Common/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Invites/InviteMetadata.cs rename to src/Discord.Net/Entities/Invites/InviteMetadata.cs diff --git a/src/Discord.Net/Common/Entities/Messages/Attachment.cs b/src/Discord.Net/Entities/Messages/Attachment.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Attachment.cs rename to src/Discord.Net/Entities/Messages/Attachment.cs diff --git a/src/Discord.Net/Common/Entities/Messages/Direction.cs b/src/Discord.Net/Entities/Messages/Direction.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Direction.cs rename to src/Discord.Net/Entities/Messages/Direction.cs diff --git a/src/Discord.Net/Common/Entities/Messages/Embed.cs b/src/Discord.Net/Entities/Messages/Embed.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Embed.cs rename to src/Discord.Net/Entities/Messages/Embed.cs diff --git a/src/Discord.Net/Common/Entities/Messages/EmbedProvider.cs b/src/Discord.Net/Entities/Messages/EmbedProvider.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/EmbedProvider.cs rename to src/Discord.Net/Entities/Messages/EmbedProvider.cs diff --git a/src/Discord.Net/Common/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/EmbedThumbnail.cs rename to src/Discord.Net/Entities/Messages/EmbedThumbnail.cs diff --git a/src/Discord.Net/Common/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/IMessage.cs rename to src/Discord.Net/Entities/Messages/IMessage.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net/Entities/Permissions/ChannelPermission.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/ChannelPermission.cs rename to src/Discord.Net/Entities/Permissions/ChannelPermission.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs rename to src/Discord.Net/Entities/Permissions/ChannelPermissions.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/GuildPermission.cs b/src/Discord.Net/Entities/Permissions/GuildPermission.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/GuildPermission.cs rename to src/Discord.Net/Entities/Permissions/GuildPermission.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs rename to src/Discord.Net/Entities/Permissions/GuildPermissions.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/Overwrite.cs b/src/Discord.Net/Entities/Permissions/Overwrite.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/Overwrite.cs rename to src/Discord.Net/Entities/Permissions/Overwrite.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/OverwritePermissions.cs rename to src/Discord.Net/Entities/Permissions/OverwritePermissions.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/PermValue.cs b/src/Discord.Net/Entities/Permissions/PermValue.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/PermValue.cs rename to src/Discord.Net/Entities/Permissions/PermValue.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/PermissionTarget.cs b/src/Discord.Net/Entities/Permissions/PermissionTarget.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/PermissionTarget.cs rename to src/Discord.Net/Entities/Permissions/PermissionTarget.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/Permissions.cs rename to src/Discord.Net/Entities/Permissions/Permissions.cs diff --git a/src/Discord.Net/Common/Entities/Roles/Color.cs b/src/Discord.Net/Entities/Roles/Color.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Roles/Color.cs rename to src/Discord.Net/Entities/Roles/Color.cs diff --git a/src/Discord.Net/Common/Entities/Roles/IRole.cs b/src/Discord.Net/Entities/Roles/IRole.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Roles/IRole.cs rename to src/Discord.Net/Entities/Roles/IRole.cs diff --git a/src/Discord.Net/Common/Entities/Users/Connection.cs b/src/Discord.Net/Entities/Users/Connection.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/Connection.cs rename to src/Discord.Net/Entities/Users/Connection.cs diff --git a/src/Discord.Net/Common/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/Game.cs rename to src/Discord.Net/Entities/Users/Game.cs diff --git a/src/Discord.Net/Common/Entities/Users/IConnection.cs b/src/Discord.Net/Entities/Users/IConnection.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/IConnection.cs rename to src/Discord.Net/Entities/Users/IConnection.cs diff --git a/src/Discord.Net/Common/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/IGuildUser.cs rename to src/Discord.Net/Entities/Users/IGuildUser.cs diff --git a/src/Discord.Net/Common/Entities/Users/ISelfUser.cs b/src/Discord.Net/Entities/Users/ISelfUser.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/ISelfUser.cs rename to src/Discord.Net/Entities/Users/ISelfUser.cs diff --git a/src/Discord.Net/Common/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/IUser.cs rename to src/Discord.Net/Entities/Users/IUser.cs diff --git a/src/Discord.Net/Common/Entities/Users/IVoiceState.cs.old b/src/Discord.Net/Entities/Users/IVoiceState.cs.old similarity index 100% rename from src/Discord.Net/Common/Entities/Users/IVoiceState.cs.old rename to src/Discord.Net/Entities/Users/IVoiceState.cs.old diff --git a/src/Discord.Net/Common/Entities/Users/StreamType.cs b/src/Discord.Net/Entities/Users/StreamType.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/StreamType.cs rename to src/Discord.Net/Entities/Users/StreamType.cs diff --git a/src/Discord.Net/Common/Entities/Users/UserStatus.cs b/src/Discord.Net/Entities/Users/UserStatus.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/UserStatus.cs rename to src/Discord.Net/Entities/Users/UserStatus.cs diff --git a/src/Discord.Net/EventExtensions.cs b/src/Discord.Net/EventExtensions.cs new file mode 100644 index 000000000..f961866d1 --- /dev/null +++ b/src/Discord.Net/EventExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class EventExtensions + { + public static async Task Raise(this Func eventHandler) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke().ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T arg) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg).ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2).ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + } + } +} diff --git a/src/Discord.Net/Common/Events/LogMessageEventArgs.cs b/src/Discord.Net/Events/LogMessageEventArgs.cs similarity index 100% rename from src/Discord.Net/Common/Events/LogMessageEventArgs.cs rename to src/Discord.Net/Events/LogMessageEventArgs.cs diff --git a/src/Discord.Net/Common/Events/SentRequestEventArgs.cs b/src/Discord.Net/Events/SentRequestEventArgs.cs similarity index 100% rename from src/Discord.Net/Common/Events/SentRequestEventArgs.cs rename to src/Discord.Net/Events/SentRequestEventArgs.cs diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 3bdf82a43..21c3c477c 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,5 +1,6 @@ using Discord.API; -using Discord.Net.Rest; +using Discord.Net.Queue; +using Discord.WebSocket.Data; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -9,15 +10,20 @@ namespace Discord //TODO: Add docstrings public interface IDiscordClient { - TokenType AuthTokenType { get; } + LoginState LoginState { get; } + ConnectionState ConnectionState { get; } + DiscordApiClient ApiClient { get; } - IRestClient RestClient { get; } IRequestQueue RequestQueue { get; } + IDataStore DataStore { get; } Task Login(string email, string password); Task Login(TokenType tokenType, string token, bool validateToken = true); Task Logout(); + Task Connect(); + Task Disconnect(); + Task GetChannel(ulong id); Task> GetDMChannels(); diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs index f8679d0ec..ccc7f06f7 100644 --- a/src/Discord.Net/Logging/ILogger.cs +++ b/src/Discord.Net/Logging/ILogger.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Discord.Logging { @@ -6,28 +7,28 @@ namespace Discord.Logging { LogSeverity Level { get; } - void Log(LogSeverity severity, string message, Exception exception = null); - void Log(LogSeverity severity, FormattableString message, Exception exception = null); - void Log(LogSeverity severity, Exception exception); + Task Log(LogSeverity severity, string message, Exception exception = null); + Task Log(LogSeverity severity, FormattableString message, Exception exception = null); + Task Log(LogSeverity severity, Exception exception); - void Error(string message, Exception exception = null); - void Error(FormattableString message, Exception exception = null); - void Error(Exception exception); + Task Error(string message, Exception exception = null); + Task Error(FormattableString message, Exception exception = null); + Task Error(Exception exception); - void Warning(string message, Exception exception = null); - void Warning(FormattableString message, Exception exception = null); - void Warning(Exception exception); + Task Warning(string message, Exception exception = null); + Task Warning(FormattableString message, Exception exception = null); + Task Warning(Exception exception); - void Info(string message, Exception exception = null); - void Info(FormattableString message, Exception exception = null); - void Info(Exception exception); + Task Info(string message, Exception exception = null); + Task Info(FormattableString message, Exception exception = null); + Task Info(Exception exception); - void Verbose(string message, Exception exception = null); - void Verbose(FormattableString message, Exception exception = null); - void Verbose(Exception exception); + Task Verbose(string message, Exception exception = null); + Task Verbose(FormattableString message, Exception exception = null); + Task Verbose(Exception exception); - void Debug(string message, Exception exception = null); - void Debug(FormattableString message, Exception exception = null); - void Debug(Exception exception); + Task Debug(string message, Exception exception = null); + Task Debug(FormattableString message, Exception exception = null); + Task Debug(Exception exception); } } diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 0c183071d..bcbfbd20a 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Discord.Logging { @@ -6,107 +7,107 @@ namespace Discord.Logging { public LogSeverity Level { get; } - public event EventHandler Message = delegate { }; + public event Func Message; internal LogManager(LogSeverity minSeverity) { Level = minSeverity; } - public void Log(LogSeverity severity, string source, string message, Exception ex = null) + public async Task Log(LogSeverity severity, string source, string message, Exception ex = null) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, message, ex)); + await Message.Raise(new LogMessageEventArgs(severity, source, message, ex)).ConfigureAwait(false); } - public void Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) + public async Task Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, message.ToString(), ex)); + await Message.Raise(new LogMessageEventArgs(severity, source, message.ToString(), ex)).ConfigureAwait(false); } - public void Log(LogSeverity severity, string source, Exception ex) + public async Task Log(LogSeverity severity, string source, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, null, ex)); + await Message.Raise(new LogMessageEventArgs(severity, source, null, ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, string message, Exception ex) + async Task ILogger.Log(LogSeverity severity, string message, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", message, ex)); + await Message.Raise(new LogMessageEventArgs(severity, "Discord", message, ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) + async Task ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)); + await Message.Raise(new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, Exception ex) + async Task ILogger.Log(LogSeverity severity, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", null, ex)); + await Message.Raise(new LogMessageEventArgs(severity, "Discord", null, ex)).ConfigureAwait(false); } - public void Error(string source, string message, Exception ex = null) + public Task Error(string source, string message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); - public void Error(string source, FormattableString message, Exception ex = null) + public Task Error(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); - public void Error(string source, Exception ex) + public Task Error(string source, Exception ex) => Log(LogSeverity.Error, source, ex); - void ILogger.Error(string message, Exception ex) + Task ILogger.Error(string message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); - void ILogger.Error(FormattableString message, Exception ex) + Task ILogger.Error(FormattableString message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); - void ILogger.Error(Exception ex) + Task ILogger.Error(Exception ex) => Log(LogSeverity.Error, "Discord", ex); - public void Warning(string source, string message, Exception ex = null) + public Task Warning(string source, string message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); - public void Warning(string source, FormattableString message, Exception ex = null) + public Task Warning(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); - public void Warning(string source, Exception ex) + public Task Warning(string source, Exception ex) => Log(LogSeverity.Warning, source, ex); - void ILogger.Warning(string message, Exception ex) + Task ILogger.Warning(string message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); - void ILogger.Warning(FormattableString message, Exception ex) + Task ILogger.Warning(FormattableString message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); - void ILogger.Warning(Exception ex) + Task ILogger.Warning(Exception ex) => Log(LogSeverity.Warning, "Discord", ex); - public void Info(string source, string message, Exception ex = null) + public Task Info(string source, string message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); - public void Info(string source, FormattableString message, Exception ex = null) + public Task Info(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); - public void Info(string source, Exception ex) + public Task Info(string source, Exception ex) => Log(LogSeverity.Info, source, ex); - void ILogger.Info(string message, Exception ex) + Task ILogger.Info(string message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); - void ILogger.Info(FormattableString message, Exception ex) + Task ILogger.Info(FormattableString message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); - void ILogger.Info(Exception ex) + Task ILogger.Info(Exception ex) => Log(LogSeverity.Info, "Discord", ex); - public void Verbose(string source, string message, Exception ex = null) + public Task Verbose(string source, string message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); - public void Verbose(string source, FormattableString message, Exception ex = null) + public Task Verbose(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); - public void Verbose(string source, Exception ex) + public Task Verbose(string source, Exception ex) => Log(LogSeverity.Verbose, source, ex); - void ILogger.Verbose(string message, Exception ex) + Task ILogger.Verbose(string message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); - void ILogger.Verbose(FormattableString message, Exception ex) + Task ILogger.Verbose(FormattableString message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); - void ILogger.Verbose(Exception ex) + Task ILogger.Verbose(Exception ex) => Log(LogSeverity.Verbose, "Discord", ex); - public void Debug(string source, string message, Exception ex = null) + public Task Debug(string source, string message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, FormattableString message, Exception ex = null) + public Task Debug(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, Exception ex) + public Task Debug(string source, Exception ex) => Log(LogSeverity.Debug, source, ex); - void ILogger.Debug(string message, Exception ex) + Task ILogger.Debug(string message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); - void ILogger.Debug(FormattableString message, Exception ex) + Task ILogger.Debug(FormattableString message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); - void ILogger.Debug(Exception ex) + Task ILogger.Debug(Exception ex) => Log(LogSeverity.Debug, "Discord", ex); internal Logger CreateLogger(string name) => new Logger(this, name); diff --git a/src/Discord.Net/LoginState.cs b/src/Discord.Net/LoginState.cs new file mode 100644 index 000000000..42b6ecac9 --- /dev/null +++ b/src/Discord.Net/LoginState.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum LoginState : byte + { + LoggedOut, + LoggingIn, + LoggedIn, + LoggingOut + } +} diff --git a/src/Discord.Net/Common/MentionUtils.cs b/src/Discord.Net/MentionUtils.cs similarity index 100% rename from src/Discord.Net/Common/MentionUtils.cs rename to src/Discord.Net/MentionUtils.cs diff --git a/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs b/src/Discord.Net/Net/Queue/BucketGroup.cs similarity index 71% rename from src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs rename to src/Discord.Net/Net/Queue/BucketGroup.cs index 54c3e717d..161f08432 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs +++ b/src/Discord.Net/Net/Queue/BucketGroup.cs @@ -1,4 +1,4 @@ -namespace Discord.Net.Rest +namespace Discord.Net.Queue { internal enum BucketGroup { diff --git a/src/Discord.Net/Net/Queue/GlobalBucket.cs b/src/Discord.Net/Net/Queue/GlobalBucket.cs new file mode 100644 index 000000000..d1e011ffd --- /dev/null +++ b/src/Discord.Net/Net/Queue/GlobalBucket.cs @@ -0,0 +1,12 @@ +namespace Discord.Net.Queue +{ + public enum GlobalBucket + { + General, + Login, + DirectMessage, + SendEditMessage, + Gateway, + UpdateStatus + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs b/src/Discord.Net/Net/Queue/GuildBucket.cs similarity index 83% rename from src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs rename to src/Discord.Net/Net/Queue/GuildBucket.cs index cb4c0d9a8..4089fd1e7 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs +++ b/src/Discord.Net/Net/Queue/GuildBucket.cs @@ -1,4 +1,4 @@ -namespace Discord.Net.Rest +namespace Discord.Net.Queue { public enum GuildBucket { diff --git a/src/Discord.Net/Net/Queue/IQueuedRequest.cs b/src/Discord.Net/Net/Queue/IQueuedRequest.cs new file mode 100644 index 000000000..e5575046e --- /dev/null +++ b/src/Discord.Net/Net/Queue/IQueuedRequest.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal interface IQueuedRequest + { + TaskCompletionSource Promise { get; } + CancellationToken CancelToken { get; } + Task Send(); + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs b/src/Discord.Net/Net/Queue/IRequestQueue.cs similarity index 87% rename from src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs rename to src/Discord.Net/Net/Queue/IRequestQueue.cs index 67adbf924..75a820934 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs +++ b/src/Discord.Net/Net/Queue/IRequestQueue.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { //TODO: Add docstrings public interface IRequestQueue diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs similarity index 92% rename from src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs rename to src/Discord.Net/Net/Queue/RequestQueue.cs index 9bf2189fe..91ad91711 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -4,7 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { public class RequestQueue : IRequestQueue { @@ -15,12 +15,8 @@ namespace Discord.Net.Rest private CancellationToken? _parentToken; private CancellationToken _cancelToken; - public IRestClient RestClient { get; } - - public RequestQueue(IRestClient restClient) + public RequestQueue() { - RestClient = restClient; - _lock = new SemaphoreSlim(1, 1); _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; _guildBuckets = new Dictionary[Enum.GetValues(typeof(GuildBucket)).Length]; @@ -38,12 +34,10 @@ namespace Discord.Net.Rest finally { Unlock(); } } - internal async Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) { RequestQueueBucket bucket; - request.CancelToken = _cancelToken; - await Lock().ConfigureAwait(false); try { @@ -66,6 +60,9 @@ namespace Discord.Net.Rest case GlobalBucket.General: return new RequestQueueBucket(this, bucket, int.MaxValue, 0); //Catch-all case GlobalBucket.Login: return new RequestQueueBucket(this, bucket, 1, 1); //TODO: Is this actual logins or token validations too? case GlobalBucket.DirectMessage: return new RequestQueueBucket(this, bucket, 5, 5); + case GlobalBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, 50, 10); + case GlobalBucket.Gateway: return new RequestQueueBucket(this, bucket, 120, 60); + case GlobalBucket.UpdateStatus: return new RequestQueueBucket(this, bucket, 5, 1, GlobalBucket.Gateway); default: throw new ArgumentException($"Unknown global bucket: {bucket}", nameof(bucket)); } @@ -75,7 +72,7 @@ namespace Discord.Net.Rest switch (bucket) { //Per Guild - case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5); + case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5, GlobalBucket.SendEditMessage); case GuildBucket.DeleteMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 1); case GuildBucket.DeleteMessages: return new RequestQueueBucket(this, bucket, guildId, 1, 1); case GuildBucket.ModifyMember: return new RequestQueueBucket(this, bucket, guildId, 10, 10); //TODO: Is this all users or just roles? diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs similarity index 86% rename from src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs rename to src/Discord.Net/Net/Queue/RequestQueueBucket.cs index 708e3251c..7b05fb0fe 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -5,15 +5,17 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { + //TODO: Implement bucket chaining internal class RequestQueueBucket { private readonly RequestQueue _parent; private readonly BucketGroup _bucketGroup; + private readonly GlobalBucket? _chainedBucket; private readonly int _bucketId; private readonly ulong _guildId; - private readonly ConcurrentQueue _queue; + private readonly ConcurrentQueue _queue; private readonly SemaphoreSlim _lock; private Task _resetTask; private bool _waitingToProcess; @@ -23,31 +25,32 @@ namespace Discord.Net.Rest public int WindowSeconds { get; } public int WindowCount { get; private set; } - public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds) - : this(parent, windowMaxCount, windowSeconds) + public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) + : this(parent, windowMaxCount, windowSeconds, chainedBucket) { _bucketGroup = BucketGroup.Global; _bucketId = (int)bucket; _guildId = 0; } - public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds) - : this(parent, windowMaxCount, windowSeconds) + public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) + : this(parent, windowMaxCount, windowSeconds, chainedBucket) { _bucketGroup = BucketGroup.Guild; _bucketId = (int)bucket; _guildId = guildId; } - private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds) + private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) { _parent = parent; WindowMaxCount = windowMaxCount; WindowSeconds = windowSeconds; - _queue = new ConcurrentQueue(); + _chainedBucket = chainedBucket; + _queue = new ConcurrentQueue(); _lock = new SemaphoreSlim(1, 1); _id = new System.Random().Next(0, int.MaxValue); } - public void Queue(RestRequest request) + public void Queue(IQueuedRequest request) { _queue.Enqueue(request); } @@ -68,7 +71,7 @@ namespace Discord.Net.Rest _waitingToProcess = false; while (true) { - RestRequest request; + IQueuedRequest request; //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort if (WindowCount == WindowMaxCount) return; @@ -81,11 +84,7 @@ namespace Discord.Net.Rest request.Promise.SetException(new OperationCanceledException(request.CancelToken)); else { - Stream stream; - if (request.IsMultipart) - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false); - else - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.CancelToken, request.Json, request.HeaderOnly).ConfigureAwait(false); + Stream stream = await request.Send().ConfigureAwait(false); request.Promise.SetResult(stream); } } @@ -157,7 +156,7 @@ namespace Discord.Net.Rest public void Clear() { //Assume this obj is under lock - RestRequest request; + IQueuedRequest request; while (_queue.TryDequeue(out request)) { } } diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs new file mode 100644 index 000000000..49c557dfd --- /dev/null +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -0,0 +1,53 @@ +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class RestRequest : IQueuedRequest + { + public IRestClient Client { get; } + public string Method { get; } + public string Endpoint { get; } + public string Json { get; } + public bool HeaderOnly { get; } + public IReadOnlyDictionary MultipartParams { get; } + public TaskCompletionSource Promise { get; } + public CancellationToken CancelToken { get; internal set; } + + public bool IsMultipart => MultipartParams != null; + + public RestRequest(IRestClient client, string method, string endpoint, string json, bool headerOnly) + : this(client, method, endpoint, headerOnly) + { + Json = json; + } + + public RestRequest(IRestClient client, string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) + : this(client, method, endpoint, headerOnly) + { + MultipartParams = multipartParams; + } + + private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly) + { + Client = client; + Method = method; + Endpoint = endpoint; + Json = null; + MultipartParams = null; + HeaderOnly = headerOnly; + Promise = new TaskCompletionSource(); + } + + public async Task Send() + { + if (IsMultipart) + return await Client.Send(Method, Endpoint, MultipartParams, HeaderOnly).ConfigureAwait(false); + else + return await Client.Send(Method, Endpoint, Json, HeaderOnly).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs new file mode 100644 index 000000000..47a00ad68 --- /dev/null +++ b/src/Discord.Net/Net/Queue/WebSocketRequest.cs @@ -0,0 +1,34 @@ +using Discord.Net.WebSockets; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class WebSocketRequest : IQueuedRequest + { + public IWebSocketClient Client { get; } + public byte[] Data { get; } + public int Offset { get; } + public int Bytes { get; } + public bool IsText { get; } + public CancellationToken CancelToken { get; } + public TaskCompletionSource Promise { get; } + + public WebSocketRequest(byte[] data, bool isText, CancellationToken cancelToken) : this(data, 0, data.Length, isText, cancelToken) { } + public WebSocketRequest(byte[] data, int offset, int length, bool isText, CancellationToken cancelToken) + { + Data = data; + Offset = offset; + Bytes = length; + IsText = isText; + Promise = new TaskCompletionSource(); + } + + public async Task Send() + { + await Client.Send(Data, Offset, Bytes, IsText).ConfigureAwait(false); + return null; + } + } +} diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 07a29342b..8166f23a7 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -17,6 +17,8 @@ namespace Discord.Net.Rest protected readonly HttpClient _client; protected readonly string _baseUrl; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; protected bool _isDisposed; public DefaultRestClient(string baseUrl) @@ -32,6 +34,7 @@ namespace Discord.Net.Rest }); SetHeader("accept-encoding", "gzip, deflate"); + _parentToken = CancellationToken.None; } protected virtual void Dispose(bool disposing) { @@ -53,19 +56,28 @@ namespace Discord.Net.Rest if (value != null) _client.DefaultRequestHeaders.Add(key, value); } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } - public async Task Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false) + public async Task Send(string method, string endpoint, bool headerOnly = false) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); + } + public async Task Send(string method, string endpoint, string json, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { - if (json != null) - restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); } } - - public async Task Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary multipartParams, bool headerOnly = false) + public async Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) @@ -112,14 +124,15 @@ namespace Discord.Net.Rest } } restRequest.Content = content; - return await SendInternal(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); } } - private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + private async Task SendInternal(HttpRequestMessage request, bool headerOnly) { while (true) { + var cancelToken = _cancelToken; //It's okay if another thread changes this, causes a retry to abort HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); int statusCode = (int)response.StatusCode; diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index 93740fc95..25b577688 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -9,8 +9,10 @@ namespace Discord.Net.Rest public interface IRestClient { void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); - Task Send(string method, string endpoint, CancellationToken cancelToken, string json = null, bool headerOnly = false); - Task Send(string method, string endpoint, CancellationToken cancelToken, IReadOnlyDictionary multipartParams, bool headerOnly = false); + Task Send(string method, string endpoint, bool headerOnly = false); + Task Send(string method, string endpoint, string json, bool headerOnly = false); + Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false); } } diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs deleted file mode 100644 index 9ce523155..000000000 --- a/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Net.Rest -{ - public enum GlobalBucket - { - General, - Login, - DirectMessage - } -} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs deleted file mode 100644 index 715333873..000000000 --- a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Rest -{ - internal class RestRequest - { - public string Method { get; } - public string Endpoint { get; } - public string Json { get; } - public bool HeaderOnly { get; } - public CancellationToken CancelToken { get; internal set; } - public IReadOnlyDictionary MultipartParams { get; } - public TaskCompletionSource Promise { get; } - - public bool IsMultipart => MultipartParams != null; - - public RestRequest(string method, string endpoint, string json, bool headerOnly) - : this(method, endpoint, headerOnly) - { - Json = json; - } - - public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) - : this(method, endpoint, headerOnly) - { - MultipartParams = multipartParams; - } - - private RestRequest(string method, string endpoint, bool headerOnly) - { - Method = method; - Endpoint = endpoint; - Json = null; - MultipartParams = null; - HeaderOnly = headerOnly; - Promise = new TaskCompletionSource(); - } - } -} diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 23b2fd015..8eee70c0c 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.ComponentModel; using System.IO; using System.Net.WebSockets; @@ -13,26 +12,25 @@ namespace Discord.Net.WebSockets { public const int ReceiveChunkSize = 12 * 1024; //12KB public const int SendChunkSize = 4 * 1024; //4KB - protected const int HR_TIMEOUT = -2147012894; + private const int HR_TIMEOUT = -2147012894; - public event EventHandler BinaryMessage = delegate { }; - public event EventHandler TextMessage = delegate { }; - - protected readonly ConcurrentQueue _sendQueue; - protected readonly ClientWebSocket _client; - protected Task _receiveTask, _sendTask; - protected CancellationTokenSource _cancelToken; - protected bool _isDisposed; + public event Func BinaryMessage; + public event Func TextMessage; + + private readonly ClientWebSocket _client; + private Task _task; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed; public DefaultWebSocketClient() { - _sendQueue = new ConcurrentQueue(); - _client = new ClientWebSocket(); _client.Options.Proxy = null; _client.Options.KeepAliveInterval = TimeSpan.Zero; + _parentToken = CancellationToken.None; } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { @@ -46,135 +44,106 @@ namespace Discord.Net.WebSockets Dispose(true); } - public async Task Connect(string host, CancellationToken cancelToken) + public async Task Connect(string host) { await Disconnect().ConfigureAwait(false); - _cancelToken = new CancellationTokenSource(); - var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken.Token, cancelToken).Token; + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - await _client.ConnectAsync(new Uri(host), combinedToken).ConfigureAwait(false); - _receiveTask = ReceiveAsync(combinedToken); - _sendTask = SendAsync(combinedToken); + await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); + _task = Run(_cancelToken); } public async Task Disconnect() { - _cancelToken.Cancel(); - - string ignored; - while (_sendQueue.TryDequeue(out ignored)) { } + _cancelTokenSource.Cancel(); _client.Abort(); - - var receiveTask = _receiveTask ?? Task.CompletedTask; - var sendTask = _sendTask ?? Task.CompletedTask; - await Task.WhenAll(receiveTask, sendTask).ConfigureAwait(false); + + await (_task ?? Task.CompletedTask).ConfigureAwait(false); } public void SetHeader(string key, string value) { _client.Options.SetRequestHeader(key, value); } - - public void QueueMessage(string message) + public void SetCancelToken(CancellationToken cancelToken) { - _sendQueue.Enqueue(message); + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - //TODO: Check this code - private Task ReceiveAsync(CancellationToken cancelToken) + public async Task Send(byte[] data, int offset, int count, bool isText) { - return Task.Run(async () => + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); + + for (int i = 0; i < frameCount; i++, offset += SendChunkSize) { - var buffer = new ArraySegment(new byte[ReceiveChunkSize]); - var stream = new MemoryStream(); + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; try { - while (!cancelToken.IsCancellationRequested) - { - WebSocketReceiveResult result = null; - do - { - if (cancelToken.IsCancellationRequested) return; - - try - { - result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); - } - catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) - { - throw new Exception($"Connection timed out."); - } - - if (result.MessageType == WebSocketMessageType.Close) - throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); - else - stream.Write(buffer.Array, 0, result.Count); - - } - while (result == null || !result.EndOfMessage); - - var array = stream.ToArray(); - if (result.MessageType == WebSocketMessageType.Binary) - BinaryMessage(this, new BinaryMessageEventArgs(array)); - else if (result.MessageType == WebSocketMessageType.Text) - { - string text = Encoding.UTF8.GetString(array, 0, array.Length); - TextMessage(this, new TextMessageEventArgs(text)); - } - - stream.Position = 0; - stream.SetLength(0); - } + await _client.SendAsync(new ArraySegment(data, offset, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); } - catch (OperationCanceledException) { } - }); + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + return; + } + } } //TODO: Check this code - private Task SendAsync(CancellationToken cancelToken) + private async Task Run(CancellationToken cancelToken) { - return Task.Run(async () => - { - byte[] bytes = new byte[SendChunkSize]; + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); + var stream = new MemoryStream(); - try + try + { + while (!cancelToken.IsCancellationRequested) { - while (!cancelToken.IsCancellationRequested) + WebSocketReceiveResult result = null; + do { - string json; - while (_sendQueue.TryDequeue(out json)) + if (cancelToken.IsCancellationRequested) return; + + try + { + result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { - int byteCount = Encoding.UTF8.GetBytes(json, 0, json.Length, bytes, 0); - int frameCount = (int)Math.Ceiling((double)byteCount / SendChunkSize); - - int offset = 0; - for (int i = 0; i < frameCount; i++, offset += SendChunkSize) - { - bool isLast = i == (frameCount - 1); - - int count; - if (isLast) - count = byteCount - (i * SendChunkSize); - else - count = SendChunkSize; - - try - { - await _client.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); - } - catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) - { - return; - } - } + throw new Exception("Connection timed out."); } - await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); + + if (result.MessageType == WebSocketMessageType.Close) + throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); + else + stream.Write(buffer.Array, 0, result.Count); + + } + while (result == null || !result.EndOfMessage); + + var array = stream.ToArray(); + if (result.MessageType == WebSocketMessageType.Binary) + await BinaryMessage.Raise(new BinaryMessageEventArgs(array)).ConfigureAwait(false); + else if (result.MessageType == WebSocketMessageType.Text) + { + string text = Encoding.UTF8.GetString(array, 0, array.Length); + await TextMessage.Raise(new TextMessageEventArgs(text)).ConfigureAwait(false); } + + stream.Position = 0; + stream.SetLength(0); } - catch (OperationCanceledException) { } - }); + } + catch (OperationCanceledException) { } } } } diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs index e6bbeb402..20cfd0da8 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -7,13 +7,15 @@ namespace Discord.Net.WebSockets //TODO: Add ETF public interface IWebSocketClient { - event EventHandler BinaryMessage; - event EventHandler TextMessage; + event Func BinaryMessage; + event Func TextMessage; void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); - Task Connect(string host, CancellationToken cancelToken); + Task Connect(string host); Task Disconnect(); - void QueueMessage(string message); + + Task Send(byte[] data, int offset, int length, bool isText); } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs index ab41404b9..88f467221 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs @@ -1,4 +1,4 @@ namespace Discord.Net.WebSockets { - public delegate IWebSocketClient WebSocketProvider(string baseUrl); + public delegate IWebSocketClient WebSocketProvider(); } diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index 90ab61ea1..992b52c92 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -1,5 +1,7 @@ using Discord.API.Rest; using Discord.Logging; +using Discord.Net; +using Discord.Net.Queue; using Discord.Net.Rest; using System; using System.Collections.Generic; @@ -15,39 +17,37 @@ namespace Discord.Rest //TODO: Log Logins/Logouts public sealed class DiscordClient : IDiscordClient, IDisposable { - public event EventHandler Log; - public event EventHandler LoggedIn, LoggedOut; + public event Func Log; + public event Func LoggedIn, LoggedOut; private readonly Logger _discordLogger, _restLogger; private readonly SemaphoreSlim _connectionLock; private readonly RestClientProvider _restClientProvider; private readonly LogManager _log; - private CancellationTokenSource _cancelTokenSource; + private readonly RequestQueue _requestQueue; private bool _isDisposed; private SelfUser _currentUser; - public bool IsLoggedIn { get; private set; } + public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } - - public TokenType AuthTokenType => ApiClient.AuthTokenType; - public IRestClient RestClient => ApiClient.RestClient; - public IRequestQueue RequestQueue => ApiClient.RequestQueue; + + public IRequestQueue RequestQueue => _requestQueue; public DiscordClient(DiscordConfig config = null) { if (config == null) config = new DiscordConfig(); - - _restClientProvider = config.RestClientProvider; - + _log = new LogManager(config.LogLevel); - _log.Message += (s, e) => Log.Raise(this, e); + _log.Message += async e => await Log.Raise(e).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); _restLogger = _log.CreateLogger("Rest"); _connectionLock = new SemaphoreSlim(1, 1); - ApiClient = new API.DiscordApiClient(_restClientProvider); - ApiClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + _requestQueue = new RequestQueue(); + + ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); + ApiClient.SentRequest += async e => await _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms").ConfigureAwait(false); } public async Task Login(string email, string password) @@ -55,7 +55,7 @@ namespace Discord.Rest await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(email, password).ConfigureAwait(false); + await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false); } finally { _connectionLock.Release(); } } @@ -64,55 +64,51 @@ namespace Discord.Rest await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); + await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(string email, string password) + private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) { - if (IsLoggedIn) + if (LoginState != LoginState.LoggedOut) await LogoutInternal().ConfigureAwait(false); - try - { - _cancelTokenSource = new CancellationTokenSource(); + LoginState = LoginState.LoggingIn; - var args = new LoginParams { Email = email, Password = password }; - await ApiClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false); - await CompleteLogin(false).ConfigureAwait(false); - } - catch { await LogoutInternal().ConfigureAwait(false); throw; } - } - private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) - { - if (IsLoggedIn) - await LogoutInternal().ConfigureAwait(false); try { - _cancelTokenSource = new CancellationTokenSource(); + if (useEmail) + { + var args = new LoginParams { Email = email, Password = password }; + await ApiClient.Login(args).ConfigureAwait(false); + } + else + await ApiClient.Login(tokenType, token).ConfigureAwait(false); - await ApiClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false); - await CompleteLogin(validateToken).ConfigureAwait(false); - } - catch { await LogoutInternal().ConfigureAwait(false); throw; } - } - private async Task CompleteLogin(bool validateToken) - { - if (validateToken) - { - try + if (validateToken) { - await ApiClient.ValidateToken().ConfigureAwait(false); + try + { + await ApiClient.ValidateToken().ConfigureAwait(false); + } + catch (HttpException ex) + { + throw new ArgumentException("Token validation failed", nameof(token), ex); + } } - catch { await ApiClient.Logout().ConfigureAwait(false); } + + LoginState = LoginState.LoggedIn; } - - IsLoggedIn = true; - LoggedIn.Raise(this); + catch (Exception) + { + await LogoutInternal().ConfigureAwait(false); + throw; + } + + await LoggedIn.Raise().ConfigureAwait(false); } public async Task Logout() { - _cancelTokenSource?.Cancel(); await _connectionLock.WaitAsync().ConfigureAwait(false); try { @@ -122,22 +118,16 @@ namespace Discord.Rest } private async Task LogoutInternal() { - bool wasLoggedIn = IsLoggedIn; - - if (_cancelTokenSource != null) - { - try { _cancelTokenSource.Cancel(false); } - catch { } - } + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; await ApiClient.Logout().ConfigureAwait(false); + _currentUser = null; - if (wasLoggedIn) - { - IsLoggedIn = false; - LoggedOut.Raise(this); - } + LoginState = LoginState.LoggedOut; + + await LoggedOut.Raise().ConfigureAwait(false); } public async Task> GetConnections() @@ -251,16 +241,15 @@ namespace Discord.Rest void Dispose(bool disposing) { if (!_isDisposed) - { - if (disposing) - _cancelTokenSource.Dispose(); _isDisposed = true; - } } public void Dispose() => Dispose(true); - API.DiscordApiClient IDiscordClient.ApiClient => ApiClient; + ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + WebSocket.Data.IDataStore IDiscordClient.DataStore => null; + Task IDiscordClient.Connect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); } + Task IDiscordClient.Disconnect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); } async Task IDiscordClient.GetChannel(ulong id) => await GetChannel(id).ConfigureAwait(false); async Task> IDiscordClient.GetDMChannels() diff --git a/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs b/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs new file mode 100644 index 000000000..0b1c78317 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.WebSocket.Data +{ + public delegate IDataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount); +} diff --git a/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs b/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs new file mode 100644 index 000000000..1308792a6 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs @@ -0,0 +1,110 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.WebSocket.Data +{ + public class DefaultDataStore : IDataStore + { + private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 + private const double AverageRolesPerGuild = 5; //Source: Googie2149 //TODO: Get a real value + private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 + private const double CollectionMultiplier = 1.05; //Add buffer to handle growth + private const double CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2? + + private ConcurrentDictionary _channels; + private ConcurrentDictionary _guilds; + private ConcurrentDictionary _roles; + private ConcurrentDictionary _users; + + public IEnumerable Channels => _channels.Select(x => x.Value); + public IEnumerable Guilds => _guilds.Select(x => x.Value); + public IEnumerable Roles => _roles.Select(x => x.Value); + public IEnumerable Users => _users.Select(x => x.Value); + + public DefaultDataStore(int guildCount, int dmChannelCount) + { + _channels = new ConcurrentDictionary(1, (int)((guildCount * AverageChannelsPerGuild + dmChannelCount) * CollectionMultiplier)); + _guilds = new ConcurrentDictionary(1, (int)(guildCount * CollectionMultiplier)); + _roles = new ConcurrentDictionary(1, (int)(guildCount * AverageRolesPerGuild * CollectionMultiplier)); + _users = new ConcurrentDictionary(1, (int)(guildCount * AverageUsersPerGuild * CollectionMultiplier)); + } + + public Channel GetChannel(ulong id) + { + Channel channel; + if (_channels.TryGetValue(id, out channel)) + return channel; + return null; + } + public void AddChannel(Channel channel) + { + _channels[channel.Id] = channel; + } + public Channel RemoveChannel(ulong id) + { + Channel channel; + if (_channels.TryRemove(id, out channel)) + return channel; + return null; + } + + public Guild GetGuild(ulong id) + { + Guild guild; + if (_guilds.TryGetValue(id, out guild)) + return guild; + return null; + } + public void AddGuild(Guild guild) + { + _guilds[guild.Id] = guild; + } + public Guild RemoveGuild(ulong id) + { + Guild guild; + if (_guilds.TryRemove(id, out guild)) + return guild; + return null; + } + + public Role GetRole(ulong id) + { + Role role; + if (_roles.TryGetValue(id, out role)) + return role; + return null; + } + public void AddRole(Role role) + { + _roles[role.Id] = role; + } + public Role RemoveRole(ulong id) + { + Role role; + if (_roles.TryRemove(id, out role)) + return role; + return null; + } + + public User GetUser(ulong id) + { + User user; + if (_users.TryGetValue(id, out user)) + return user; + return null; + } + public void AddUser(User user) + { + _users[user.Id] = user; + } + public User RemoveUser(ulong id) + { + User user; + if (_users.TryRemove(id, out user)) + return user; + return null; + } + } +} diff --git a/src/Discord.Net/WebSocket/Data/IDataStore.cs b/src/Discord.Net/WebSocket/Data/IDataStore.cs new file mode 100644 index 000000000..1a9d6e450 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/IDataStore.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Discord.WebSocket.Data +{ + public interface IDataStore + { + IEnumerable Channels { get; } + IEnumerable Guilds { get; } + IEnumerable Roles { get; } + IEnumerable Users { get; } + + Channel GetChannel(ulong id); + void AddChannel(Channel channel); + Channel RemoveChannel(ulong id); + + Guild GetGuild(ulong id); + void AddGuild(Guild guild); + Guild RemoveGuild(ulong id); + + Role GetRole(ulong id); + void AddRole(Role role); + Role RemoveRole(ulong id); + + User GetUser(ulong id); + void AddUser(User user); + User RemoveUser(ulong id); + } +} diff --git a/src/Discord.Net/WebSocket/Data/SharedDataStore.cs b/src/Discord.Net/WebSocket/Data/SharedDataStore.cs new file mode 100644 index 000000000..8512a2679 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/SharedDataStore.cs @@ -0,0 +1,7 @@ +namespace Discord.WebSocket.Data +{ + //TODO: Implement + /*public class SharedDataStore + { + }*/ +} diff --git a/src/Discord.Net/WebSocket/DiscordClient.cs b/src/Discord.Net/WebSocket/DiscordClient.cs index 99fba0107..71b327a74 100644 --- a/src/Discord.Net/WebSocket/DiscordClient.cs +++ b/src/Discord.Net/WebSocket/DiscordClient.cs @@ -1,11 +1,19 @@ -using Discord.API.Rest; +using Discord.API; +using Discord.API.Gateway; +using Discord.API.Rest; using Discord.Logging; -using Discord.Net.Rest; +using Discord.Net; +using Discord.Net.Converters; +using Discord.Net.Queue; +using Discord.Net.WebSockets; +using Discord.WebSocket.Data; +using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.IO.Compression; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,75 +22,67 @@ namespace Discord.WebSocket { //TODO: Docstrings //TODO: Log Logins/Logouts + //TODO: Do a final namespace and file structure review public sealed class DiscordClient : IDiscordClient, IDisposable { - public event EventHandler Log; - public event EventHandler LoggedIn, LoggedOut; - public event EventHandler Connected, Disconnected; - public event EventHandler VoiceConnected, VoiceDisconnected; - - public event EventHandler ChannelCreated, ChannelDestroyed; - public event EventHandler ChannelUpdated; - public event EventHandler MessageReceived, MessageDeleted; - public event EventHandler MessageUpdated; - public event EventHandler RoleCreated, RoleDeleted; - public event EventHandler RoleUpdated; - public event EventHandler JoinedGuild, LeftGuild; - public event EventHandler GuildAvailable, GuildUnavailable; - public event EventHandler GuildUpdated; - public event EventHandler CurrentUserUpdated; - public event EventHandler UserJoined, UserLeft; - public event EventHandler UserBanned, UserUnbanned; - public event EventHandler UserUpdated; - public event EventHandler UserIsTyping; + public event Func Log; + public event Func LoggedIn, LoggedOut; + public event Func Connected, Disconnected; + //public event Func VoiceConnected, VoiceDisconnected; + public event Func ChannelCreated, ChannelDestroyed; + public event Func ChannelUpdated; + public event Func MessageReceived, MessageDeleted; + public event Func MessageUpdated; + public event Func RoleCreated, RoleDeleted; + public event Func RoleUpdated; + public event Func JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable; + public event Func GuildUpdated; + public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; + public event Func UserUpdated; + public event Func UserIsTyping; + private readonly ConcurrentQueue _largeGuilds; private readonly Logger _discordLogger, _gatewayLogger; private readonly SemaphoreSlim _connectionLock; - private readonly RestClientProvider _restClientProvider; + private readonly DataStoreProvider _dataStoreProvider; private readonly LogManager _log; + private readonly RequestQueue _requestQueue; + private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; - private IReadOnlyDictionary _voiceRegions; - private CancellationTokenSource _cancelTokenSource; + private ImmutableDictionary _voiceRegions; + private string _sessionId; private bool _isDisposed; - private SelfUser _currentUser; - private ConcurrentDictionary _guilds; - private ConcurrentDictionary _channels; - private ConcurrentDictionary _dmChannels; //Key = RecipientId - private ConcurrentDictionary _users; public int ShardId { get; } - public bool IsLoggedIn { get; private set; } + public LoginState LoginState { get; private set; } + public ConnectionState ConnectionState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } + public IWebSocketClient GatewaySocket { get; private set; } + public IDataStore DataStore { get; private set; } public SelfUser CurrentUser { get; private set; } - //public GatewaySocket GatewaySocket { get; private set; } internal int MessageCacheSize { get; private set; } internal bool UsePermissionCache { get; private set; } - - public TokenType AuthTokenType => ApiClient.AuthTokenType; - public IRestClient RestClient => ApiClient.RestClient; - public IRequestQueue RequestQueue => ApiClient.RequestQueue; - public IEnumerable Guilds => _guilds.Values; - public IEnumerable Channels => _channels.Values; - public IEnumerable DMChannels => _dmChannels.Values; - public IEnumerable VoiceRegions => _voiceRegions.Values; - - //public bool IsConnected => GatewaySocket.State == ConnectionState.Connected; + + public IRequestQueue RequestQueue => _requestQueue; + public IEnumerable Guilds => DataStore.Guilds; + public IEnumerable DMChannels => DataStore.Users.Select(x => x.DMChannel).Where(x => x != null); + public IEnumerable VoiceRegions => _voiceRegions.Select(x => x.Value); public DiscordClient(DiscordSocketConfig config = null) { if (config == null) config = new DiscordSocketConfig(); - - _restClientProvider = config.RestClientProvider; + ShardId = config.ShardId; _totalShards = config.TotalShards; _connectionTimeout = config.ConnectionTimeout; _reconnectDelay = config.ReconnectDelay; _failedReconnectDelay = config.FailedReconnectDelay; + _dataStoreProvider = config.DataStoreProvider; MessageCacheSize = config.MessageCacheSize; UsePermissionCache = config.UsePermissionsCache; @@ -90,26 +90,52 @@ namespace Discord.WebSocket _largeThreshold = config.LargeThreshold; _log = new LogManager(config.LogLevel); - _log.Message += (s, e) => Log.Raise(this, e); + _log.Message += async e => await Log.Raise(e).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); _gatewayLogger = _log.CreateLogger("Gateway"); _connectionLock = new SemaphoreSlim(1, 1); - ApiClient = new API.DiscordApiClient(_restClientProvider); - ApiClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + _requestQueue = new RequestQueue(); + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + ApiClient = new API.DiscordApiClient(config.RestClientProvider, config.WebSocketProvider, _serializer, _requestQueue); + ApiClient.SentRequest += async e => await _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); + GatewaySocket = config.WebSocketProvider(); + GatewaySocket.BinaryMessage += async e => + { + using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 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)) + await ProcessMessage(reader.ReadToEnd()).ConfigureAwait(false); + } + }; + GatewaySocket.TextMessage += async e => await ProcessMessage(e.Message).ConfigureAwait(false); - _channels = new ConcurrentDictionary(1, 100); - _dmChannels = new ConcurrentDictionary(1, 100); - _guilds = new ConcurrentDictionary(1, 25); - _users = new ConcurrentDictionary(1, 250); + _voiceRegions = ImmutableDictionary.Create(); + _largeGuilds = new ConcurrentQueue(); } + + void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + ApiClient?.Dispose(); + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); public async Task Login(string email, string password) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(email, password).ConfigureAwait(false); + await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false); } finally { _connectionLock.Release(); } } @@ -118,87 +144,133 @@ namespace Discord.WebSocket await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); + await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(string email, string password) + private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) { - if (IsLoggedIn) + if (LoginState != LoginState.LoggedOut) await LogoutInternal().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + try { - _cancelTokenSource = new CancellationTokenSource(); + if (useEmail) + { + var args = new LoginParams { Email = email, Password = password }; + await ApiClient.Login(args).ConfigureAwait(false); + } + else + await ApiClient.Login(tokenType, token).ConfigureAwait(false); + + if (validateToken) + { + try + { + await ApiClient.ValidateToken().ConfigureAwait(false); + var gateway = await ApiClient.GetGateway(); + var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); - var args = new LoginParams { Email = email, Password = password }; - await ApiClient.Login(args, _cancelTokenSource.Token).ConfigureAwait(false); - await CompleteLogin(false).ConfigureAwait(false); + await GatewaySocket.Connect(gateway.Url).ConfigureAwait(false); + } + catch (HttpException ex) + { + throw new ArgumentException("Token validation failed", nameof(token), ex); + } + } + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternal().ConfigureAwait(false); + throw; } - catch { await LogoutInternal().ConfigureAwait(false); throw; } + + await LoggedIn.Raise().ConfigureAwait(false); } - private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) + + public async Task Logout() { - if (IsLoggedIn) - await LogoutInternal().ConfigureAwait(false); + await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _cancelTokenSource = new CancellationTokenSource(); - - await ApiClient.Login(tokenType, token, _cancelTokenSource.Token).ConfigureAwait(false); - await CompleteLogin(validateToken).ConfigureAwait(false); + await LogoutInternal().ConfigureAwait(false); } - catch { await LogoutInternal().ConfigureAwait(false); throw; } + finally { _connectionLock.Release(); } } - private async Task CompleteLogin(bool validateToken) + private async Task LogoutInternal() { - if (validateToken) - { - try - { - await ApiClient.ValidateToken().ConfigureAwait(false); - var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; - } - catch { await ApiClient.Logout().ConfigureAwait(false); } - } + if (ConnectionState != ConnectionState.Disconnected) + await DisconnectInternal().ConfigureAwait(false); + + await ApiClient.Logout().ConfigureAwait(false); + + _voiceRegions = ImmutableDictionary.Create(); + CurrentUser = null; - IsLoggedIn = true; - LoggedIn.Raise(this); + LoginState = LoginState.LoggedOut; + + await LoggedOut.Raise().ConfigureAwait(false); } - public async Task Logout() + public async Task Connect() { - _cancelTokenSource?.Cancel(); await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LogoutInternal().ConfigureAwait(false); + await ConnectInternal().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LogoutInternal() + private async Task ConnectInternal() { - bool wasLoggedIn = IsLoggedIn; + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting."); - if (_cancelTokenSource != null) + ConnectionState = ConnectionState.Connecting; + try { - try { _cancelTokenSource.Cancel(false); } - catch { } + await ApiClient.Connect().ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternal().ConfigureAwait(false); + throw; } - await ApiClient.Logout().ConfigureAwait(false); - _channels.Clear(); - _dmChannels.Clear(); - _guilds.Clear(); - _users.Clear(); - _currentUser = null; + await Connected.Raise().ConfigureAwait(false); + } - if (wasLoggedIn) + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - IsLoggedIn = false; - LoggedOut.Raise(this); + await DisconnectInternal().ConfigureAwait(false); } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternal() + { + ulong guildId; + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + await ApiClient.Disconnect().ConfigureAwait(false); + while (_largeGuilds.TryDequeue(out guildId)) { } + + ConnectionState = ConnectionState.Disconnected; + + await Disconnected.Raise().ConfigureAwait(false); } public async Task> GetConnections() @@ -207,12 +279,9 @@ namespace Discord.WebSocket return models.Select(x => new Connection(x)); } - public IChannel GetChannel(ulong id) + public async Task GetChannel(ulong id) { - IChannel channel; - if (_channels.TryGetValue(id, out channel)) - return channel; - return null; + return DataStore.GetChannel(id); } public async Task GetInvite(string inviteIdOrXkcd) @@ -223,12 +292,9 @@ namespace Discord.WebSocket return null; } - public Guild GetGuild(ulong id) + public async Task GetGuild(ulong id) { - Guild guild; - if (_guilds.TryGetValue(id, out guild)) - return guild; - return null; + return DataStore.GetGuild(id); } public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { @@ -237,16 +303,13 @@ namespace Discord.WebSocket return new Guild(this, model); } - public User GetUser(ulong id) + public async Task GetUser(ulong id) { - User user; - if (_users.TryGetValue(id, out user)) - return user; - return null; + return DataStore.GetUser(id); } - public User GetUser(string username, ushort discriminator) + public async Task GetUser(string username, ushort discriminator) { - return _users.Where(x => x.Value.Discriminator == discriminator && x.Value.Username == username).Select(x => x.Value).FirstOrDefault(); + return DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); } public async Task> QueryUsers(string query, int limit) { @@ -254,7 +317,7 @@ namespace Discord.WebSocket return models.Select(x => new User(this, x)); } - public VoiceRegion GetVoiceRegion(string id) + public async Task GetVoiceRegion(string id) { VoiceRegion region; if (_voiceRegions.TryGetValue(id, out region)) @@ -262,44 +325,572 @@ namespace Discord.WebSocket return null; } - void Dispose(bool disposing) + private async Task ProcessMessage(string json) { - if (!_isDisposed) + var msg = JsonConvert.DeserializeObject(json); + try { - if (disposing) - _cancelTokenSource.Dispose(); - _isDisposed = true; + switch (msg.Type) + { + //Global + case "READY": + { + //TODO: None of this is really threadsafe - should only replace the cache collections when they have been fully populated + //TODO: Store guilds even if they're unavailable + //TODO: Make downloading large guilds optional + + var data = msg.Payload.ToObject(_serializer); + var store = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + + _sessionId = data.SessionId; + var currentUser = new SelfUser(this, data.User); + store.AddUser(currentUser); + + for (int i = 0; i < data.Guilds.Length; i++) + { + var model = data.Guilds[i]; + var guild = new Guild(this, model); + store.AddGuild(guild); + + foreach (var channel in guild.Channels) + store.AddChannel(channel); + + /*if (model.IsLarge) + _largeGuilds.Enqueue(model.Id);*/ + } + + for (int i = 0; i < data.PrivateChannels.Length; i++) + { + var model = data.PrivateChannels[i]; + var recipient = new User(this, model.Recipient); + var channel = new DMChannel(this, recipient, model); + + recipient.DMChannel = channel; + store.AddChannel(channel); + } + + CurrentUser = currentUser; + DataStore = store; + } + break; + + //Servers + case "GUILD_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); + if (data.Unavailable != true) + { + var server = AddServer(data.Id); + server.Update(data); + + if (data.Unavailable != false) + { + _gatewayLogger.Info($"GUILD_CREATE: {server.Path}"); + JoinedServer.Raise(server); + } + else + _gatewayLogger.Info($"GUILD_AVAILABLE: {server.Path}"); + + if (!data.IsLarge) + await GuildAvailable.Raise(server); + else + _largeServers.Enqueue(data.Id); + }*/ + } + break; + case "GUILD_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.Id); + if (server != null) + { + var before = Config.EnablePreUpdateEvents ? server.Clone() : null; + server.Update(data); + _gatewayLogger.Info($"GUILD_UPDATE: {server.Path}"); + await GuildUpdated.Raise(before, server); + } + else + _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild.");*/ + } + break; + case "GUILD_DELETE": + { + /*var data = msg.Payload.ToObject(Serializer); + Server server = RemoveServer(data.Id); + if (server != null) + { + if (data.Unavailable != true) + _gatewayLogger.Info($"GUILD_DELETE: {server.Path}"); + else + _gatewayLogger.Info($"GUILD_UNAVAILABLE: {server.Path}"); + + OnServerUnavailable(server); + if (data.Unavailable != true) + OnLeftServer(server); + } + else + _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild.");*/ + } + break; + + //Channels + case "CHANNEL_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); + + Channel channel = null; + if (data.GuildId != null) + { + var server = GetServer(data.GuildId.Value); + if (server != null) + channel = server.AddChannel(data.Id, true); + else + _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + } + else + channel = AddPrivateChannel(data.Id, data.Recipient.Id); + if (channel != null) + { + channel.Update(data); + _gatewayLogger.Info($"CHANNEL_CREATE: {channel.Path}"); + ChannelCreated.Raise(new ChannelEventArgs(channel)); + }*/ + } + break; + case "CHANNEL_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.Id); + if (channel != null) + { + var before = Config.EnablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data); + _gateway_gatewayLogger.Info($"CHANNEL_UPDATE: {channel.Path}"); + OnChannelUpdated(before, channel); + } + else + _gateway_gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel.");*/ + } + break; + case "CHANNEL_DELETE": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = RemoveChannel(data.Id); + if (channel != null) + { + _gateway_gatewayLogger.Info($"CHANNEL_DELETE: {channel.Path}"); + OnChannelDestroyed(channel); + } + else + _gateway_gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel.");*/ + } + break; + + //Members + case "GUILD_MEMBER_ADD": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.AddUser(data.User.Id, true, true); + user.Update(data); + user.UpdateActivity(); + _gatewayLogger.Info($"GUILD_MEMBER_ADD: {user.Path}"); + OnUserJoined(user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild.");*/ + } + break; + case "GUILD_MEMBER_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.GetUser(data.User.Id); + if (user != null) + { + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + _gatewayLogger.Info($"GUILD_MEMBER_UPDATE: {user.Path}"); + OnUserUpdated(before, user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild.");*/ + } + break; + case "GUILD_MEMBER_REMOVE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.RemoveUser(data.User.Id); + if (user != null) + { + _gatewayLogger.Info($"GUILD_MEMBER_REMOVE: {user.Path}"); + OnUserLeft(user); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + } + else + _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild.");*/ + } + break; + case "GUILD_MEMBERS_CHUNK": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + foreach (var memberData in data.Members) + { + var user = server.AddUser(memberData.User.Id, true, false); + user.Update(memberData); + } + _gateway_gatewayLogger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); + + if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there + OnServerAvailable(server); + } + else + _gateway_gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild.");*/ + } + break; + + //Roles + case "GUILD_ROLE_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.AddRole(data.Data.Id); + role.Update(data.Data, false); + _gateway_gatewayLogger.Info($"GUILD_ROLE_CREATE: {role.Path}"); + OnRoleCreated(role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild.");*/ + } + break; + case "GUILD_ROLE_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.GetRole(data.Data.Id); + if (role != null) + { + var before = Config.EnablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Data, true); + _gateway_gatewayLogger.Info($"GUILD_ROLE_UPDATE: {role.Path}"); + OnRoleUpdated(before, role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild.");*/ + } + break; + case "GUILD_ROLE_DELETE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var role = server.RemoveRole(data.RoleId); + if (role != null) + { + _gateway_gatewayLogger.Info($"GUILD_ROLE_DELETE: {role.Path}"); + OnRoleDeleted(role); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + } + else + _gateway_gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild.");*/ + } + break; + + //Bans + case "GUILD_BAN_ADD": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = server.GetUser(data.User.Id); + if (user != null) + { + _gateway_gatewayLogger.Info($"GUILD_BAN_ADD: {user.Path}"); + OnUserBanned(user); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown user."); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild.");*/ + } + break; + case "GUILD_BAN_REMOVE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId.Value); + if (server != null) + { + var user = new User(this, data.User.Id, server); + user.Update(data.User); + _gateway_gatewayLogger.Info($"GUILD_BAN_REMOVE: {user.Path}"); + OnUserUnbanned(user); + } + else + _gateway_gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild.");*/ + } + break; + + //Messages + case "MESSAGE_CREATE": + { + /*var data = msg.Payload.ToObject(Serializer); + + Channel channel = GetChannel(data.ChannelId); + if (channel != null) + { + var user = channel.GetUserFast(data.Author.Id); + + if (user != null) + { + Message msg = null; + bool isAuthor = data.Author.Id == CurrentUser.Id; + //ulong nonce = 0; + + //if (data.Author.Id == _privateUser.Id && Config.UseMessageQueue) + //{ + // if (data.Nonce != null && ulong.TryParse(data.Nonce, out nonce)) + // msg = _messages[nonce]; + //} + if (msg == null) + { + msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); + //nonce = 0; + } + + //Remapped queued message + //if (nonce != 0) + //{ + // msg = _messages.Remap(nonce, data.Id); + // msg.Id = data.Id; + // RaiseMessageSent(msg); + //} + + msg.Update(data); + user.UpdateActivity(); + + _gateway_gatewayLogger.Verbose($"MESSAGE_CREATE: {channel.Path} ({user.Name ?? "Unknown"})"); + OnMessageReceived(msg); + } + else + _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + } + else + _gateway_gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel.");*/ + } + break; + case "MESSAGE_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.GetMessage(data.Id, data.Author?.Id); + var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; + msg.Update(data); + _gatewayLogger.Verbose($"MESSAGE_UPDATE: {channel.Path} ({data.Author?.Username ?? "Unknown"})"); + OnMessageUpdated(before, msg); + } + else + _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel.");*/ + } + break; + case "MESSAGE_DELETE": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.RemoveMessage(data.Id); + _gatewayLogger.Verbose($"MESSAGE_DELETE: {channel.Path} ({msg.User?.Name ?? "Unknown"})"); + OnMessageDeleted(msg); + } + else + _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel.");*/ + } + break; + + //Statuses + case "PRESENCE_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + User user; + Server server; + if (data.GuildId == null) + { + server = null; + user = GetPrivateChannel(data.User.Id)?.Recipient; + } + else + { + server = GetServer(data.GuildId.Value); + if (server == null) + { + _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown server."); + break; + } + else + user = server.GetUser(data.User.Id); + } + + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"PRESENCE_UPDATE: {user.Path}"); + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + OnUserUpdated(before, user); + } + //else //Occurs when a user leaves a server + // _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ + } + break; + case "TYPING_START": + { + /*var data = msg.Payload.ToObject(Serializer); + var channel = GetChannel(data.ChannelId); + if (channel != null) + { + User user; + if (channel.IsPrivate) + { + if (channel.Recipient.Id == data.UserId) + user = channel.Recipient; + else + break; + } + else + user = channel.Server.GetUser(data.UserId); + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"TYPING_START: {channel.Path} ({user.Name})"); + OnUserIsTypingUpdated(channel, user); + user.UpdateActivity(); + } + } + else + _gatewayLogger.Warning("TYPING_START referenced an unknown channel.");*/ + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + var server = GetServer(data.GuildId); + if (server != null) + { + var user = server.GetUser(data.UserId); + if (user != null) + { + if (Config.LogLevel == LogSeverity.Debug) + _gatewayLogger.Debug($"VOICE_STATE_UPDATE: {user.Path}"); + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + //_gatewayLogger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); + OnUserUpdated(before, user); + } + //else //Occurs when a user leaves a server + // _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); + } + else + _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown server.");*/ + } + break; + + //Settings + case "USER_UPDATE": + { + /*var data = msg.Payload.ToObject(Serializer); + if (data.Id == CurrentUser.Id) + { + var before = Config.EnablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data); + foreach (var server in _servers) + server.Value.CurrentUser.Update(data); + _gatewayLogger.Info($"USER_UPDATE"); + OnProfileUpdated(before, CurrentUser); + }*/ + } + break; + + //Handled in GatewaySocket + case "RESUMED": + break; + + //Ignored + case "USER_SETTINGS_UPDATE": + case "MESSAGE_ACK": //TODO: Add (User only) + case "GUILD_EMOJIS_UPDATE": //TODO: Add + case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add + case "VOICE_SERVER_UPDATE": //TODO: Add + _gatewayLogger.Debug($"{msg.Type} [Ignored]"); + break; + + //Others + default: + _gatewayLogger.Warning($"Unknown message type: {msg.Type}"); + break; + } + } + catch (Exception ex) + { + _gatewayLogger.Error($"Error handling {msg.Type} event", ex); } } - public void Dispose() => Dispose(true); - - API.DiscordApiClient IDiscordClient.ApiClient => ApiClient; - Task IDiscordClient.GetChannel(ulong id) - => Task.FromResult(GetChannel(id)); + async Task IDiscordClient.GetChannel(ulong id) + => await GetChannel(id).ConfigureAwait(false); Task> IDiscordClient.GetDMChannels() - => Task.FromResult>(DMChannels); + => Task.FromResult>(DMChannels.ToImmutableArray()); async Task> IDiscordClient.GetConnections() => await GetConnections().ConfigureAwait(false); async Task IDiscordClient.GetInvite(string inviteIdOrXkcd) => await GetInvite(inviteIdOrXkcd).ConfigureAwait(false); - Task IDiscordClient.GetGuild(ulong id) - => Task.FromResult(GetGuild(id)); + async Task IDiscordClient.GetGuild(ulong id) + => await GetGuild(id).ConfigureAwait(false); Task> IDiscordClient.GetGuilds() - => Task.FromResult>(Guilds); + => Task.FromResult>(Guilds.ToImmutableArray()); async Task IDiscordClient.CreateGuild(string name, IVoiceRegion region, Stream jpegIcon) => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUser(ulong id) - => Task.FromResult(GetUser(id)); - Task IDiscordClient.GetUser(string username, ushort discriminator) - => Task.FromResult(GetUser(username, discriminator)); + async Task IDiscordClient.GetUser(ulong id) + => await GetUser(id).ConfigureAwait(false); + async Task IDiscordClient.GetUser(string username, ushort discriminator) + => await GetUser(username, discriminator).ConfigureAwait(false); Task IDiscordClient.GetCurrentUser() => Task.FromResult(CurrentUser); async Task> IDiscordClient.QueryUsers(string query, int limit) => await QueryUsers(query, limit).ConfigureAwait(false); Task> IDiscordClient.GetVoiceRegions() - => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegion(string id) - => Task.FromResult(GetVoiceRegion(id)); + => Task.FromResult>(VoiceRegions.ToImmutableArray()); + async Task IDiscordClient.GetVoiceRegion(string id) + => await GetVoiceRegion(id).ConfigureAwait(false); } } diff --git a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs index abe76a0d7..4318bd247 100644 --- a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs @@ -1,4 +1,5 @@ using Discord.Net.WebSockets; +using Discord.WebSocket.Data; namespace Discord.WebSocket { @@ -32,8 +33,10 @@ namespace Discord.WebSocket public int LargeThreshold { get; set; } = 250; //Engines - + + /// Gets or sets the provider used to generate datastores. + public DataStoreProvider DataStoreProvider { get; set; } = (shardId, totalShards, guildCount, dmCount) => new DefaultDataStore(guildCount, dmCount); /// Gets or sets the provider used to generate new websocket connections. - public WebSocketProvider WebSocketProvider { get; set; } = null; + public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); } } diff --git a/src/Discord.Net/WebSocket/Entities/Users/User.cs b/src/Discord.Net/WebSocket/Entities/Users/User.cs index 1101dd961..a69f3e5f4 100644 --- a/src/Discord.Net/WebSocket/Entities/Users/User.cs +++ b/src/Discord.Net/WebSocket/Entities/Users/User.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket /// public string Username { get; private set; } /// - public DMChannel DMChannel { get; private set; } + public DMChannel DMChannel { get; internal set; } /// public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); @@ -58,7 +58,6 @@ namespace Discord.WebSocket var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); channel = new DMChannel(Discord, this, model); - DMChannel = channel; } return channel; } diff --git a/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs b/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs deleted file mode 100644 index 4689e44bc..000000000 --- a/src/Discord.Net/WebSocket/Events/ChannelEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class ChannelEventArgs : EventArgs - { - public IChannel Channel { get; } - - public ChannelEventArgs(IChannel channel) - { - Channel = channel; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs deleted file mode 100644 index 24d001bc3..000000000 --- a/src/Discord.Net/WebSocket/Events/ChannelUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class ChannelUpdatedEventArgs : ChannelEventArgs - { - public IChannel Before { get; } - public IChannel After => Channel; - - public ChannelUpdatedEventArgs(IChannel before, IChannel after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs b/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs deleted file mode 100644 index 97d352c31..000000000 --- a/src/Discord.Net/WebSocket/Events/CurrentUserEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class CurrentUserEventArgs : EventArgs - { - public SelfUser CurrentUser { get; } - - public CurrentUserEventArgs(SelfUser currentUser) - { - CurrentUser = currentUser; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs deleted file mode 100644 index 167278089..000000000 --- a/src/Discord.Net/WebSocket/Events/CurrentUserUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class CurrentUserUpdatedEventArgs : CurrentUserEventArgs - { - public SelfUser Before { get; } - public SelfUser After => CurrentUser; - - public CurrentUserUpdatedEventArgs(SelfUser before, SelfUser after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs b/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs deleted file mode 100644 index d9120b243..000000000 --- a/src/Discord.Net/WebSocket/Events/DisconnectedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class DisconnectedEventArgs : EventArgs - { - public bool WasUnexpected { get; } - public Exception Exception { get; } - - public DisconnectedEventArgs(bool wasUnexpected, Exception exception = null) - { - WasUnexpected = wasUnexpected; - Exception = exception; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs b/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs deleted file mode 100644 index 8c9d4dc20..000000000 --- a/src/Discord.Net/WebSocket/Events/GuildEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class GuildEventArgs : EventArgs - { - public Guild Guild { get; } - - public GuildEventArgs(Guild guild) - { - Guild = guild; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs deleted file mode 100644 index 03a7f0f75..000000000 --- a/src/Discord.Net/WebSocket/Events/GuildUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class GuildUpdatedEventArgs : GuildEventArgs - { - public Guild Before { get; } - public Guild After => Guild; - - public GuildUpdatedEventArgs(Guild before, Guild after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs b/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs deleted file mode 100644 index a1bea179a..000000000 --- a/src/Discord.Net/WebSocket/Events/MessageEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class MessageEventArgs : EventArgs - { - public Message Message { get; } - - public MessageEventArgs(Message message) - { - Message = message; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs deleted file mode 100644 index 14b41707d..000000000 --- a/src/Discord.Net/WebSocket/Events/MessageUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class MessageUpdatedEventArgs : MessageEventArgs - { - public Message Before { get; } - public Message After => Message; - - public MessageUpdatedEventArgs(Message before, Message after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs b/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs deleted file mode 100644 index 902eea1e8..000000000 --- a/src/Discord.Net/WebSocket/Events/RoleEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class RoleEventArgs : EventArgs - { - public Role Role { get; } - - public RoleEventArgs(Role role) - { - Role = role; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs deleted file mode 100644 index 45c6f05a9..000000000 --- a/src/Discord.Net/WebSocket/Events/RoleUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class RoleUpdatedEventArgs : RoleEventArgs - { - public Role Before { get; } - public Role After => Role; - - public RoleUpdatedEventArgs(Role before, Role after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs b/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs deleted file mode 100644 index a3f45d452..000000000 --- a/src/Discord.Net/WebSocket/Events/TypingEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class TypingEventArgs : EventArgs - { - public IMessageChannel Channel { get; } - public IUser User { get; } - - public TypingEventArgs(IMessageChannel channel, IUser user) - { - Channel = channel; - User = user; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/UserEventArgs.cs b/src/Discord.Net/WebSocket/Events/UserEventArgs.cs deleted file mode 100644 index 8e31a58aa..000000000 --- a/src/Discord.Net/WebSocket/Events/UserEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class UserEventArgs : EventArgs - { - public IUser User { get; } - - public UserEventArgs(IUser user) - { - User = user; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs b/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs deleted file mode 100644 index 298b0d427..000000000 --- a/src/Discord.Net/WebSocket/Events/UserUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.WebSocket -{ - public class UserUpdatedEventArgs : UserEventArgs - { - public IUser Before { get; } - public IUser After => User; - - public UserUpdatedEventArgs(IUser before, IUser after) - : base(after) - { - Before = before; - } - } -} diff --git a/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs b/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs deleted file mode 100644 index 1b10f3cd7..000000000 --- a/src/Discord.Net/WebSocket/Events/VoiceChannelEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord.WebSocket -{ - public class VoiceChannelEventArgs : EventArgs - { - public VoiceChannel Channel { get; } - - public VoiceChannelEventArgs(VoiceChannel channel) - { - Channel = channel; - } - } -} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 01b6bd4dc..a15ef430a 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,6 +1,6 @@ -{ +{ "version": "1.0.0-dev", - "description": "A Discord.Net extension adding voice support.", + "description": "An unofficial .Net API wrapper for the Discord service.", "authors": [ "RogueException" ], "packOptions": {