From 8c2fa21b8120a9fd7e6e021b4d5d1936e26f5034 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:08:35 -0300 Subject: [PATCH 001/160] Restructured to better merge REST and WebSocket entities --- README.md | 14 +- src/Discord.Net/API/Common/Connection.cs | 2 +- src/Discord.Net/API/Common/Game.cs | 2 +- src/Discord.Net/API/Common/Presence.cs | 14 + src/Discord.Net/API/Common/Relationship.cs | 14 + .../API/Common/RelationshipType.cs | 9 + src/Discord.Net/API/Common/VoiceState.cs | 2 - src/Discord.Net/API/DiscordAPIClient.cs | 466 ++++----- .../API/Gateway/Common/ExtendedGuild.cs | 24 + src/Discord.Net/API/Gateway/GatewayOpCodes.cs | 10 +- src/Discord.Net/API/Gateway/GuildBanEvent.cs | 10 + .../API/Gateway/GuildRoleDeleteEvent.cs | 12 + src/Discord.Net/API/Gateway/ReadyEvent.cs | 4 +- ...ssagesParam.cs => DeleteMessagesParams.cs} | 2 +- src/Discord.Net/API/Rest/LoginParams.cs | 12 - src/Discord.Net/API/Rest/LoginResponse.cs | 10 - .../API/Rest/ModifyCurrentUserParams.cs | 9 +- src/Discord.Net/Data/DataStoreProvider.cs | 4 + src/Discord.Net/Data/DefaultDataStore.cs | 90 ++ src/Discord.Net/Data/IDataStore.cs | 24 + src/Discord.Net/Data/SharedDataStore.cs | 11 + src/Discord.Net/{Rest => }/DiscordClient.cs | 136 +-- src/Discord.Net/DiscordConfig.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 708 ++++++++++++++ .../{WebSocket => }/DiscordSocketConfig.cs | 14 +- .../Entities/Channels/DMChannel.cs | 126 +++ .../Entities/Channels/GuildChannel.cs | 143 ++- src/Discord.Net/Entities/Channels/IChannel.cs | 4 +- .../Entities/Channels/IGuildChannel.cs | 8 +- .../Entities/Channels/IMessageChannel.cs | 22 +- .../Entities/Channels/TextChannel.cs | 116 +++ .../Entities/Channels/VoiceChannel.cs | 34 +- src/Discord.Net/Entities/Entity.cs | 16 + src/Discord.Net/Entities/Guilds/Emoji.cs | 2 +- .../{Rest => }/Entities/Guilds/Guild.cs | 222 ++--- src/Discord.Net/Entities/Guilds/GuildEmbed.cs | 18 + .../Entities/Guilds/GuildIntegration.cs | 46 +- src/Discord.Net/Entities/Guilds/IGuild.cs | 37 +- .../Entities/Guilds/IGuildEmbed.cs | 8 - src/Discord.Net/Entities/Guilds/IUserGuild.cs | 2 +- .../Entities/Guilds/IVoiceRegion.cs | 4 +- .../Entities/Guilds/IntegrationAccount.cs | 3 - .../{Rest => }/Entities/Guilds/UserGuild.cs | 30 +- .../Entities/Guilds/VoiceRegion.cs | 10 +- src/Discord.Net/Entities/IEntity.cs | 4 + src/Discord.Net/Entities/IUpdateable.cs | 2 +- src/Discord.Net/Entities/Invites/IInvite.cs | 2 +- src/Discord.Net/Entities/Invites/Invite.cs | 39 +- .../Entities/Invites/InviteMetadata.cs | 15 +- src/Discord.Net/Entities/Messages/Embed.cs | 8 +- .../Entities/Messages/EmbedProvider.cs | 8 +- .../Entities/Messages/EmbedThumbnail.cs | 13 +- src/Discord.Net/Entities/Messages/IEmbed.cs | 12 + src/Discord.Net/Entities/Messages/IMessage.cs | 15 +- .../Entities => Entities/Messages}/Message.cs | 90 +- .../Permissions/ChannelPermissions.cs | 2 +- .../Entities/Permissions/GuildPermissions.cs | 2 +- .../Entities/Permissions/Overwrite.cs | 11 +- .../Permissions/OverwritePermissions.cs | 2 +- .../Entities/Permissions/Permissions.cs | 29 +- src/Discord.Net/Entities/Roles/IRole.cs | 5 +- .../{Rest/Entities => Entities/Roles}/Role.cs | 53 +- src/Discord.Net/Entities/SnowflakeEntity.cs | 15 + src/Discord.Net/Entities/UpdateSource.cs | 9 + src/Discord.Net/Entities/Users/Connection.cs | 5 +- src/Discord.Net/Entities/Users/Game.cs | 14 +- .../{Rest => }/Entities/Users/GuildUser.cs | 86 +- src/Discord.Net/Entities/Users/IConnection.cs | 2 +- src/Discord.Net/Entities/Users/IGuildUser.cs | 8 +- src/Discord.Net/Entities/Users/IPresence.cs | 10 + src/Discord.Net/Entities/Users/IUser.cs | 6 +- .../{Rest => }/Entities/Users/SelfUser.cs | 34 +- .../{Rest => }/Entities/Users/User.cs | 48 +- .../Entities/WebSocket/CachedDMChannel.cs | 70 ++ .../Entities/WebSocket/CachedGuild.cs | 171 ++++ .../Entities/WebSocket/CachedGuildUser.cs | 16 + .../Entities/WebSocket/CachedMessage.cs | 17 + .../Entities/WebSocket/CachedPublicUser.cs | 58 ++ .../Entities/WebSocket/CachedSelfUser.cs | 16 + .../Entities/WebSocket/CachedTextChannel.cs | 73 ++ .../Entities/WebSocket/CachedVoiceChannel.cs | 38 + .../Entities/WebSocket/ICachedChannel.cs | 6 + .../Entities/WebSocket/ICachedEntity.cs | 7 + .../Entities/WebSocket/ICachedGuildChannel.cs | 6 + .../WebSocket/ICachedMessageChannel.cs | 16 + .../{Users => WebSocket}/IVoiceState.cs.old | 8 +- .../Entities/WebSocket/Presence.cs | 14 + .../Extensions/CollectionExtensions.cs | 31 + .../Extensions/DiscordClientExtensions.cs | 14 + .../{ => Extensions}/EventExtensions.cs | 2 +- src/Discord.Net/Extensions/GuildExtensions.cs | 12 + src/Discord.Net/IDiscordClient.cs | 17 +- src/Discord.Net/Logging/LogManager.cs | 7 +- src/Discord.Net/Logging/Logger.cs | 2 +- .../Net/Converters/DiscordContractResolver.cs | 24 +- .../Net/Converters/OptionalConverter.cs | 5 +- src/Discord.Net/Net/HttpException.cs | 6 +- src/Discord.Net/Net/Queue/BucketDefinition.cs | 16 + src/Discord.Net/Net/Queue/BucketGroup.cs | 2 +- src/Discord.Net/Net/Queue/GlobalBucket.cs | 5 +- src/Discord.Net/Net/Queue/IQueuedRequest.cs | 5 +- src/Discord.Net/Net/Queue/IRequestQueue.cs | 11 - src/Discord.Net/Net/Queue/RequestQueue.cs | 188 ++-- .../Net/Queue/RequestQueueBucket.cs | 217 ++--- src/Discord.Net/Net/Queue/RestRequest.cs | 13 +- src/Discord.Net/Net/Queue/WebSocketRequest.cs | 10 +- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 36 +- .../Net/WebSockets/DefaultWebsocketClient.cs | 3 +- src/Discord.Net/RequestOptions.cs | 8 + .../Rest/Entities/Channels/DMChannel.cs | 153 --- .../Rest/Entities/Channels/TextChannel.cs | 140 --- .../Rest/Entities/Guilds/GuildEmbed.cs | 34 - .../Rest/Entities/Users/PublicUser.cs | 15 - .../{ => Utilities}/ConcurrentHashSet.cs | 2 +- .../{ => Utilities}/DateTimeUtils.cs | 0 .../{API => Utilities}/IOptional.cs | 2 +- .../{ => Utilities}/MentionUtils.cs | 6 +- .../{WebSocket => Utilities}/MessageCache.cs | 60 +- .../{API => Utilities}/Optional.cs | 2 +- .../{ => Utilities}/Preconditions.cs | 0 .../WebSocket/Data/DataStoreProvider.cs | 4 - .../WebSocket/Data/DefaultDataStore.cs | 107 --- src/Discord.Net/WebSocket/Data/IDataStore.cs | 23 - .../WebSocket/Data/SharedDataStore.cs | 7 - src/Discord.Net/WebSocket/DiscordClient.cs | 889 ------------------ .../WebSocket/Entities/Channels/Channel.cs | 37 - .../WebSocket/Entities/Channels/DMChannel.cs | 144 --- .../Entities/Channels/GuildChannel.cs | 161 ---- .../Entities/Channels/TextChannel.cs | 129 --- .../Entities/Channels/VoiceChannel.cs | 53 -- .../WebSocket/Entities/Guilds/Guild.cs | 344 ------- .../Entities/Guilds/GuildIntegration.cs | 88 -- src/Discord.Net/WebSocket/Entities/Message.cs | 155 --- src/Discord.Net/WebSocket/Entities/Role.cs | 79 -- .../WebSocket/Entities/Users/GuildUser.cs | 143 --- .../WebSocket/Entities/Users/SelfUser.cs | 40 - .../WebSocket/Entities/Users/User.cs | 76 -- src/Discord.Net/WebSocket/PermissionsCache.cs | 70 -- 138 files changed, 2868 insertions(+), 4250 deletions(-) create mode 100644 src/Discord.Net/API/Common/Presence.cs create mode 100644 src/Discord.Net/API/Common/Relationship.cs create mode 100644 src/Discord.Net/API/Common/RelationshipType.cs create mode 100644 src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs create mode 100644 src/Discord.Net/API/Gateway/GuildBanEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs rename src/Discord.Net/API/Rest/{DeleteMessagesParam.cs => DeleteMessagesParams.cs} (83%) delete mode 100644 src/Discord.Net/API/Rest/LoginParams.cs delete mode 100644 src/Discord.Net/API/Rest/LoginResponse.cs create mode 100644 src/Discord.Net/Data/DataStoreProvider.cs create mode 100644 src/Discord.Net/Data/DefaultDataStore.cs create mode 100644 src/Discord.Net/Data/IDataStore.cs create mode 100644 src/Discord.Net/Data/SharedDataStore.cs rename src/Discord.Net/{Rest => }/DiscordClient.cs (55%) create mode 100644 src/Discord.Net/DiscordSocketClient.cs rename src/Discord.Net/{WebSocket => }/DiscordSocketConfig.cs (88%) create mode 100644 src/Discord.Net/Entities/Channels/DMChannel.cs rename src/Discord.Net/{Rest => }/Entities/Channels/GuildChannel.cs (57%) create mode 100644 src/Discord.Net/Entities/Channels/TextChannel.cs rename src/Discord.Net/{Rest => }/Entities/Channels/VoiceChannel.cs (52%) create mode 100644 src/Discord.Net/Entities/Entity.cs rename src/Discord.Net/{Rest => }/Entities/Guilds/Guild.cs (58%) create mode 100644 src/Discord.Net/Entities/Guilds/GuildEmbed.cs rename src/Discord.Net/{Rest => }/Entities/Guilds/GuildIntegration.cs (68%) delete mode 100644 src/Discord.Net/Entities/Guilds/IGuildEmbed.cs rename src/Discord.Net/{Rest => }/Entities/Guilds/UserGuild.cs (65%) create mode 100644 src/Discord.Net/Entities/Messages/IEmbed.cs rename src/Discord.Net/{Rest/Entities => Entities/Messages}/Message.cs (63%) rename src/Discord.Net/{Rest/Entities => Entities/Roles}/Role.cs (54%) create mode 100644 src/Discord.Net/Entities/SnowflakeEntity.cs create mode 100644 src/Discord.Net/Entities/UpdateSource.cs rename src/Discord.Net/{Rest => }/Entities/Users/GuildUser.cs (63%) create mode 100644 src/Discord.Net/Entities/Users/IPresence.cs rename src/Discord.Net/{Rest => }/Entities/Users/SelfUser.cs (60%) rename src/Discord.Net/{Rest => }/Entities/Users/User.cs (52%) create mode 100644 src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGuild.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedMessage.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedEntity.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs rename src/Discord.Net/Entities/{Users => WebSocket}/IVoiceState.cs.old (90%) create mode 100644 src/Discord.Net/Entities/WebSocket/Presence.cs create mode 100644 src/Discord.Net/Extensions/CollectionExtensions.cs create mode 100644 src/Discord.Net/Extensions/DiscordClientExtensions.cs rename src/Discord.Net/{ => Extensions}/EventExtensions.cs (98%) create mode 100644 src/Discord.Net/Extensions/GuildExtensions.cs create mode 100644 src/Discord.Net/Net/Queue/BucketDefinition.cs delete mode 100644 src/Discord.Net/Net/Queue/IRequestQueue.cs create mode 100644 src/Discord.Net/RequestOptions.cs delete mode 100644 src/Discord.Net/Rest/Entities/Channels/DMChannel.cs delete mode 100644 src/Discord.Net/Rest/Entities/Channels/TextChannel.cs delete mode 100644 src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs delete mode 100644 src/Discord.Net/Rest/Entities/Users/PublicUser.cs rename src/Discord.Net/{ => Utilities}/ConcurrentHashSet.cs (99%) rename src/Discord.Net/{ => Utilities}/DateTimeUtils.cs (100%) rename src/Discord.Net/{API => Utilities}/IOptional.cs (81%) rename src/Discord.Net/{ => Utilities}/MentionUtils.cs (94%) rename src/Discord.Net/{WebSocket => Utilities}/MessageCache.cs (57%) rename src/Discord.Net/{API => Utilities}/Optional.cs (98%) rename src/Discord.Net/{ => Utilities}/Preconditions.cs (100%) delete mode 100644 src/Discord.Net/WebSocket/Data/DataStoreProvider.cs delete mode 100644 src/Discord.Net/WebSocket/Data/DefaultDataStore.cs delete mode 100644 src/Discord.Net/WebSocket/Data/IDataStore.cs delete mode 100644 src/Discord.Net/WebSocket/Data/SharedDataStore.cs delete mode 100644 src/Discord.Net/WebSocket/DiscordClient.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Channels/Channel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Message.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Role.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs delete mode 100644 src/Discord.Net/WebSocket/Entities/Users/User.cs delete mode 100644 src/Discord.Net/WebSocket/PermissionsCache.cs diff --git a/README.md b/README.md index 06f3ae426..28111dc43 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,6 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). -##### Warning: Some of the documentation is outdated. -It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference. - ### Installation You can download Discord.Net and its extensions from NuGet: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) @@ -16,9 +13,10 @@ You can download Discord.Net and its extensions from NuGet: - [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) ### Compiling -In order to compile Discord.Net, you require at least the following: -- [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs) -- [Visual Studio 2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) -- [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows) +In order to compile Discord.Net, you require the following: +#### Visual Studio 2015 +- [VS2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) +- [.Net Core SDK + VS Plugin](https://www.microsoft.com/net/core#windows) - NuGet 3.3+ (available through Visual Studio) - +#### CLI +- [.Net Core SDK](https://www.microsoft.com/net/core#windows) \ No newline at end of file diff --git a/src/Discord.Net/API/Common/Connection.cs b/src/Discord.Net/API/Common/Connection.cs index 8022e0314..fb06ccf21 100644 --- a/src/Discord.Net/API/Common/Connection.cs +++ b/src/Discord.Net/API/Common/Connection.cs @@ -15,6 +15,6 @@ namespace Discord.API public bool Revoked { get; set; } [JsonProperty("integrations")] - public IEnumerable Integrations { get; set; } + public IReadOnlyCollection Integrations { get; set; } } } diff --git a/src/Discord.Net/API/Common/Game.cs b/src/Discord.Net/API/Common/Game.cs index a5bbbfcdc..76a18483c 100644 --- a/src/Discord.Net/API/Common/Game.cs +++ b/src/Discord.Net/API/Common/Game.cs @@ -9,6 +9,6 @@ namespace Discord.API [JsonProperty("url")] public string StreamUrl { get; set; } [JsonProperty("type")] - public StreamType StreamType { get; set; } + public StreamType? StreamType { get; set; } } } diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net/API/Common/Presence.cs new file mode 100644 index 000000000..5f2b853e6 --- /dev/null +++ b/src/Discord.Net/API/Common/Presence.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Presence + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("game")] + public Game Game { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Relationship.cs b/src/Discord.Net/API/Common/Relationship.cs new file mode 100644 index 000000000..9b52a7750 --- /dev/null +++ b/src/Discord.Net/API/Common/Relationship.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Relationship + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("type")] + public RelationshipType Type { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/RelationshipType.cs b/src/Discord.Net/API/Common/RelationshipType.cs new file mode 100644 index 000000000..53be3a98f --- /dev/null +++ b/src/Discord.Net/API/Common/RelationshipType.cs @@ -0,0 +1,9 @@ +namespace Discord.API +{ + public enum RelationshipType + { + Friend = 1, + Blocked = 2, + Pending = 4 + } +} diff --git a/src/Discord.Net/API/Common/VoiceState.cs b/src/Discord.Net/API/Common/VoiceState.cs index f584468af..e848ea919 100644 --- a/src/Discord.Net/API/Common/VoiceState.cs +++ b/src/Discord.Net/API/Common/VoiceState.cs @@ -4,8 +4,6 @@ namespace Discord.API { public class VoiceState { - [JsonProperty("guild_id")] - public ulong? GuildId { get; set; } [JsonProperty("channel_id")] public ulong ChannelId { get; set; } [JsonProperty("user_id")] diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index dee9fddf0..eaa403fa3 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -1,5 +1,6 @@ using Discord.API.Gateway; using Discord.API.Rest; +using Discord.Extensions; using Discord.Net; using Discord.Net.Converters; using Discord.Net.Queue; @@ -91,26 +92,17 @@ namespace Discord.API } } public void Dispose() => Dispose(true); - - 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) + + public async Task Login(TokenType tokenType, string token, RequestOptions options = null) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, null, false).ConfigureAwait(false); + await LoginInternal(tokenType, token, options).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(TokenType tokenType, string token, LoginParams args, bool doLogin) + private async Task LoginInternal(TokenType tokenType, string token, RequestOptions options = null) { if (LoginState != LoginState.LoggedOut) await LogoutInternal().ConfigureAwait(false); @@ -126,12 +118,6 @@ namespace Discord.API 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; _authToken = token; switch (tokenType) @@ -249,54 +235,71 @@ namespace Discord.API } //Core - public Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, null, true, bucket); - public Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, payload, true, bucket); - public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) - => SendInternal(method, endpoint, multipartArgs, true, bucket); - public async Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) + public Task Send(string method, string endpoint, + GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) + => SendInternal(method, endpoint, null, true, bucket, options); + public Task Send(string method, string endpoint, object payload, + GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) + => SendInternal(method, endpoint, payload, true, bucket, options); + public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) + => SendInternal(method, endpoint, multipartArgs, true, bucket, options); + public async Task Send(string method, string endpoint, + GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) + => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, options).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = + GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.General) + => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, options).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); - - public Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, null, true, bucket, guildId); - public Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, payload, true, bucket, guildId); - public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, multipartArgs, true, bucket, guildId); - public async Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) + => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, options).ConfigureAwait(false)); + + public Task Send(string method, string endpoint, + GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendInternal(method, endpoint, null, true, bucket, guildId, options); + public Task Send(string method, string endpoint, object payload, + GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendInternal(method, endpoint, payload, true, bucket, guildId, options); + public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendInternal(method, endpoint, multipartArgs, true, bucket, guildId, options); + public async Task Send(string method, string endpoint, + GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId) + => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, guildId, options).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, object payload, + GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId) + => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, guildId, options).ConfigureAwait(false)); + public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId, options).ConfigureAwait(false)); - private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket) - => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0); - private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GlobalBucket bucket) - => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GuildBucket bucket, ulong guildId) - => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId); - - private async Task SendInternal(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) + private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + GlobalBucket bucket, RequestOptions options) + => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0, options); + private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + GuildBucket bucket, ulong guildId, RequestOptions options) + => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + GlobalBucket bucket, RequestOptions options) + => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0, options); + private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + GuildBucket bucket, ulong guildId, RequestOptions options) + => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); + + private async Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); string json = null; if (payload != null) json = SerializeJson(payload); - var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); @@ -304,10 +307,11 @@ namespace Discord.API return responseStream; } - private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) + private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); @@ -317,36 +321,36 @@ namespace Discord.API return responseStream; } - public Task SendGateway(GatewayOpCodes opCode, object payload, GlobalBucket bucket = GlobalBucket.Gateway) - => SendGateway((int)opCode, payload, BucketGroup.Global, (int)bucket, 0); - public Task SendGateway(VoiceOpCodes opCode, object payload, GlobalBucket bucket = GlobalBucket.Gateway) - => SendGateway((int)opCode, payload, BucketGroup.Global, (int)bucket, 0); - public Task SendGateway(GatewayOpCodes opCode, object payload, GuildBucket bucket, ulong guildId) - => SendGateway((int)opCode, payload, BucketGroup.Guild, (int)bucket, guildId); - public Task SendGateway(VoiceOpCodes opCode, object payload, GuildBucket bucket, ulong guildId) - => SendGateway((int)opCode, payload, BucketGroup.Guild, (int)bucket, guildId); - private async Task SendGateway(int opCode, object payload, BucketGroup group, int bucketId, ulong guildId) + public Task SendGateway(GatewayOpCodes opCode, object payload, + GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) + => SendGateway(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); + public Task SendGateway(GatewayOpCodes opCode, object payload, + GuildBucket bucket, ulong guildId, RequestOptions options = null) + => SendGateway(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); + private async Task SendGateway(GatewayOpCodes opCode, object payload, + BucketGroup group, int bucketId, ulong guildId, RequestOptions options) { //TODO: Add ETF byte[] bytes = null; - payload = new WebSocketMessage { Operation = opCode, Payload = payload }; + payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await _requestQueue.Send(new WebSocketRequest(_gatewayClient, bytes, true), group, bucketId, guildId).ConfigureAwait(false); + await _requestQueue.Send(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await SentGatewayMessage.Raise((int)opCode).ConfigureAwait(false); } //Auth - public async Task ValidateToken() + public async Task ValidateToken(RequestOptions options = null) { - await Send("GET", "auth/login").ConfigureAwait(false); + await Send("GET", "auth/login", options: options).ConfigureAwait(false); } //Gateway - public async Task GetGateway() + public async Task GetGateway(RequestOptions options = null) { - return await Send("GET", "gateway").ConfigureAwait(false); + return await Send("GET", "gateway", options: options).ConfigureAwait(false); } - public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true) + public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) { var props = new Dictionary { @@ -359,74 +363,74 @@ namespace Discord.API LargeThreshold = largeThreshold, UseCompression = useCompression }; - await SendGateway(GatewayOpCodes.Identify, msg).ConfigureAwait(false); + await SendGateway(GatewayOpCodes.Identify, msg, options: options).ConfigureAwait(false); } //Channels - public async Task GetChannel(ulong channelId) + public async Task GetChannel(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { - return await Send("GET", $"channels/{channelId}").ConfigureAwait(false); + return await Send("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetChannel(ulong guildId, ulong channelId) + public async Task GetChannel(ulong guildId, ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { - var model = await Send("GET", $"channels/{channelId}").ConfigureAwait(false); + var model = await Send("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); if (model.GuildId != guildId) return null; return model; } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildChannels(ulong guildId) + public async Task> GetGuildChannels(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/channels").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/channels", options: options).ConfigureAwait(false); } - public async Task CreateGuildChannel(ulong guildId, CreateGuildChannelParams args) + public async Task CreateGuildChannel(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - return await Send("POST", $"guilds/{guildId}/channels", args).ConfigureAwait(false); + return await Send("POST", $"guilds/{guildId}/channels", args, options: options).ConfigureAwait(false); } - public async Task DeleteChannel(ulong channelId) + public async Task DeleteChannel(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - return await Send("DELETE", $"channels/{channelId}").ConfigureAwait(false); + return await Send("DELETE", $"channels/{channelId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyGuildChannelParams args) + public async Task ModifyGuildChannel(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args).ConfigureAwait(false); + return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyTextChannelParams args) + public async Task ModifyGuildChannel(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args).ConfigureAwait(false); + return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyVoiceChannelParams args) + public async Task ModifyGuildChannel(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -435,9 +439,9 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args).ConfigureAwait(false); + return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannels(ulong guildId, IEnumerable args) + public async Task ModifyGuildChannels(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -451,55 +455,55 @@ namespace Discord.API await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); break; default: - await Send("PATCH", $"guilds/{guildId}/channels", channels).ConfigureAwait(false); + await Send("PATCH", $"guilds/{guildId}/channels", channels, options: options).ConfigureAwait(false); break; } } //Channel Permissions - public async Task ModifyChannelPermissions(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args) + public async Task ModifyChannelPermissions(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); - await Send("PUT", $"channels/{channelId}/permissions/{targetId}", args).ConfigureAwait(false); + await Send("PUT", $"channels/{channelId}/permissions/{targetId}", args, options: options).ConfigureAwait(false); } - public async Task DeleteChannelPermission(ulong channelId, ulong targetId) + public async Task DeleteChannelPermission(ulong channelId, ulong targetId, RequestOptions options = null) { - await Send("DELETE", $"channels/{channelId}/permissions/{targetId}").ConfigureAwait(false); + await Send("DELETE", $"channels/{channelId}/permissions/{targetId}", options: options).ConfigureAwait(false); } //Guilds - public async Task GetGuild(ulong guildId) + public async Task GetGuild(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { - return await Send("GET", $"guilds/{guildId}").ConfigureAwait(false); + return await Send("GET", $"guilds/{guildId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task CreateGuild(CreateGuildParams args) + public async Task CreateGuild(CreateGuildParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); Preconditions.NotNullOrWhitespace(args.Region, nameof(args.Region)); - return await Send("POST", "guilds", args).ConfigureAwait(false); + return await Send("POST", "guilds", args, options: options).ConfigureAwait(false); } - public async Task DeleteGuild(ulong guildId) + public async Task DeleteGuild(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("DELETE", $"guilds/{guildId}").ConfigureAwait(false); + return await Send("DELETE", $"guilds/{guildId}", options: options).ConfigureAwait(false); } - public async Task LeaveGuild(ulong guildId) + public async Task LeaveGuild(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("DELETE", $"users/@me/guilds/{guildId}").ConfigureAwait(false); + return await Send("DELETE", $"users/@me/guilds/{guildId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuild(ulong guildId, ModifyGuildParams args) + public async Task ModifyGuild(ulong guildId, ModifyGuildParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -510,91 +514,91 @@ namespace Discord.API Preconditions.NotNull(args.Region, nameof(args.Region)); Preconditions.AtLeast(args.VerificationLevel, 0, nameof(args.VerificationLevel)); - return await Send("PATCH", $"guilds/{guildId}", args).ConfigureAwait(false); + return await Send("PATCH", $"guilds/{guildId}", args, options: options).ConfigureAwait(false); } - public async Task BeginGuildPrune(ulong guildId, GuildPruneParams args) + public async Task BeginGuildPrune(ulong guildId, GuildPruneParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - return await Send("POST", $"guilds/{guildId}/prune", args).ConfigureAwait(false); + return await Send("POST", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); } - public async Task GetGuildPruneCount(ulong guildId, GuildPruneParams args) + public async Task GetGuildPruneCount(ulong guildId, GuildPruneParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - return await Send("GET", $"guilds/{guildId}/prune", args).ConfigureAwait(false); + return await Send("GET", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); } //Guild Bans - public async Task> GetGuildBans(ulong guildId) + public async Task> GetGuildBans(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/bans").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/bans", options: options).ConfigureAwait(false); } - public async Task CreateGuildBan(ulong guildId, ulong userId, CreateGuildBanParams args) + public async Task CreateGuildBan(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.PruneDays, 0, nameof(args.PruneDays)); - await Send("PUT", $"guilds/{guildId}/bans/{userId}", args).ConfigureAwait(false); + await Send("PUT", $"guilds/{guildId}/bans/{userId}", args, options: options).ConfigureAwait(false); } - public async Task RemoveGuildBan(ulong guildId, ulong userId) + public async Task RemoveGuildBan(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); - await Send("DELETE", $"guilds/{guildId}/bans/{userId}").ConfigureAwait(false); + await Send("DELETE", $"guilds/{guildId}/bans/{userId}", options: options).ConfigureAwait(false); } //Guild Embeds - public async Task GetGuildEmbed(ulong guildId) + public async Task GetGuildEmbed(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { - return await Send("GET", $"guilds/{guildId}/embed").ConfigureAwait(false); + return await Send("GET", $"guilds/{guildId}/embed", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task ModifyGuildEmbed(ulong guildId, ModifyGuildEmbedParams args) + public async Task ModifyGuildEmbed(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("PATCH", $"guilds/{guildId}/embed", args).ConfigureAwait(false); + return await Send("PATCH", $"guilds/{guildId}/embed", args, options: options).ConfigureAwait(false); } //Guild Integrations - public async Task> GetGuildIntegrations(ulong guildId) + public async Task> GetGuildIntegrations(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/integrations").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); } - public async Task CreateGuildIntegration(ulong guildId, CreateGuildIntegrationParams args) + public async Task CreateGuildIntegration(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); - return await Send("POST", $"guilds/{guildId}/integrations").ConfigureAwait(false); + return await Send("POST", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); } - public async Task DeleteGuildIntegration(ulong guildId, ulong integrationId) + public async Task DeleteGuildIntegration(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - return await Send("DELETE", $"guilds/{guildId}/integrations/{integrationId}").ConfigureAwait(false); + return await Send("DELETE", $"guilds/{guildId}/integrations/{integrationId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildIntegration(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args) + public async Task ModifyGuildIntegration(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); @@ -602,74 +606,74 @@ namespace Discord.API Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior)); Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod)); - return await Send("PATCH", $"guilds/{guildId}/integrations/{integrationId}", args).ConfigureAwait(false); + return await Send("PATCH", $"guilds/{guildId}/integrations/{integrationId}", args, options: options).ConfigureAwait(false); } - public async Task SyncGuildIntegration(ulong guildId, ulong integrationId) + public async Task SyncGuildIntegration(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - return await Send("POST", $"guilds/{guildId}/integrations/{integrationId}/sync").ConfigureAwait(false); + return await Send("POST", $"guilds/{guildId}/integrations/{integrationId}/sync", options: options).ConfigureAwait(false); } //Guild Invites - public async Task GetInvite(string inviteIdOrXkcd) + public async Task GetInvite(string inviteIdOrXkcd, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteIdOrXkcd, nameof(inviteIdOrXkcd)); try { - return await Send("GET", $"invites/{inviteIdOrXkcd}").ConfigureAwait(false); + return await Send("GET", $"invites/{inviteIdOrXkcd}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildInvites(ulong guildId) + public async Task> GetGuildInvites(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/invites").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/invites", options: options).ConfigureAwait(false); } - public async Task GetChannelInvites(ulong channelId) + public async Task GetChannelInvites(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - return await Send("GET", $"channels/{channelId}/invites").ConfigureAwait(false); + return await Send("GET", $"channels/{channelId}/invites", options: options).ConfigureAwait(false); } - public async Task CreateChannelInvite(ulong channelId, CreateChannelInviteParams args) + public async Task CreateChannelInvite(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); - return await Send("POST", $"channels/{channelId}/invites", args).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/invites", args, options: options).ConfigureAwait(false); } - public async Task DeleteInvite(string inviteCode) + public async Task DeleteInvite(string inviteCode, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - return await Send("DELETE", $"invites/{inviteCode}").ConfigureAwait(false); + return await Send("DELETE", $"invites/{inviteCode}", options: options).ConfigureAwait(false); } - public async Task AcceptInvite(string inviteCode) + public async Task AcceptInvite(string inviteCode, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - await Send("POST", $"invites/{inviteCode}").ConfigureAwait(false); + await Send("POST", $"invites/{inviteCode}", options: options).ConfigureAwait(false); } //Guild Members - public async Task GetGuildMember(ulong guildId, ulong userId) + public async Task GetGuildMember(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); try { - return await Send("GET", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false); + return await Send("GET", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildMembers(ulong guildId, GetGuildMembersParams args) + public async Task> GetGuildMembers(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -689,7 +693,7 @@ namespace Discord.API { int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; string endpoint = $"guilds/{guildId}/members?limit={runLimit}&offset={offset}"; - var models = await Send("GET", endpoint).ConfigureAwait(false); + var models = await Send("GET", endpoint, options: options).ConfigureAwait(false); //Was this an empty batch? if (models.Length == 0) break; @@ -704,49 +708,49 @@ namespace Discord.API } if (result.Count > 1) - return result.SelectMany(x => x); + return result.SelectMany(x => x).ToImmutableArray(); else if (result.Count == 1) return result[0]; else - return Array.Empty(); + return ImmutableArray.Create(); } - public async Task RemoveGuildMember(ulong guildId, ulong userId) + public async Task RemoveGuildMember(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); - await Send("DELETE", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false); + await Send("DELETE", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args) + public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); - await Send("PATCH", $"guilds/{guildId}/members/{userId}", args, GuildBucket.ModifyMember, guildId).ConfigureAwait(false); + await Send("PATCH", $"guilds/{guildId}/members/{userId}", args, GuildBucket.ModifyMember, guildId, options: options).ConfigureAwait(false); } //Guild Roles - public async Task> GetGuildRoles(ulong guildId) + public async Task> GetGuildRoles(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/roles").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); } - public async Task CreateGuildRole(ulong guildId) + public async Task CreateGuildRole(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("POST", $"guilds/{guildId}/roles").ConfigureAwait(false); + return await Send("POST", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); } - public async Task DeleteGuildRole(ulong guildId, ulong roleId) + public async Task DeleteGuildRole(ulong guildId, ulong roleId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); - await Send("DELETE", $"guilds/{guildId}/roles/{roleId}").ConfigureAwait(false); + await Send("DELETE", $"guilds/{guildId}/roles/{roleId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildRole(ulong guildId, ulong roleId, ModifyGuildRoleParams args) + public async Task ModifyGuildRole(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); @@ -755,9 +759,9 @@ namespace Discord.API Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - return await Send("PATCH", $"guilds/{guildId}/roles/{roleId}", args).ConfigureAwait(false); + return await Send("PATCH", $"guilds/{guildId}/roles/{roleId}", args, options: options).ConfigureAwait(false); } - public async Task> ModifyGuildRoles(ulong guildId, IEnumerable args) + public async Task> ModifyGuildRoles(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -766,16 +770,31 @@ namespace Discord.API switch (roles.Length) { case 0: - return Array.Empty(); + return ImmutableArray.Create(); case 1: return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); default: - return await Send>("PATCH", $"guilds/{guildId}/roles", args).ConfigureAwait(false); + return await Send>("PATCH", $"guilds/{guildId}/roles", args, options: options).ConfigureAwait(false); } } //Messages - public async Task> GetChannelMessages(ulong channelId, GetChannelMessagesParams args) + public async Task GetChannelMessage(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + //TODO: Improve when Discord adds support + var msgs = await GetChannelMessages(channelId, new GetChannelMessagesParams { Limit = 1, RelativeDirection = Direction.Before, RelativeMessageId = messageId + 1 }).ConfigureAwait(false); + return msgs.FirstOrDefault(); + + /*try + { + return await Send("GET", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; }*/ + } + public async Task> GetChannelMessages(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -798,7 +817,7 @@ namespace Discord.API endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; else endpoint = $"channels/{channelId}/messages?limit={runCount}"; - var models = await Send("GET", endpoint).ConfigureAwait(false); + var models = await Send("GET", endpoint, options: options).ConfigureAwait(false); //Was this an empty batch? if (models.Length == 0) break; @@ -813,26 +832,26 @@ namespace Discord.API if (i > 1) { if (args.RelativeDirection == Direction.Before) - return result.Take(i).SelectMany(x => x); + return result.Take(i).SelectMany(x => x).ToImmutableArray(); else - return result.Take(i).Reverse().SelectMany(x => x); + return result.Take(i).Reverse().SelectMany(x => x).ToImmutableArray(); } else if (i == 1) return result[0]; else - return Array.Empty(); + return ImmutableArray.Create(); } - public Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args) + public Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); return CreateMessageInternal(guildId, channelId, args); } - public Task CreateDMMessage(ulong channelId, CreateMessageParams args) + public Task CreateDMMessage(ulong channelId, CreateMessageParams args, RequestOptions options = null) { return CreateMessageInternal(0, channelId, args); } - public async Task CreateMessageInternal(ulong guildId, ulong channelId, CreateMessageParams args) + public async Task CreateMessageInternal(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -841,21 +860,21 @@ namespace Discord.API throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); if (guildId != 0) - return await Send("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); } - public Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args) + public Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); return UploadFileInternal(guildId, channelId, file, args); } - public Task UploadDMFile(ulong channelId, Stream file, UploadFileParams args) + public Task UploadDMFile(ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { return UploadFileInternal(0, channelId, file, args); } - private async Task UploadFileInternal(ulong guildId, ulong channelId, Stream file, UploadFileParams args) + private async Task UploadFileInternal(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -867,47 +886,49 @@ namespace Discord.API } if (guildId != 0) - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GlobalBucket.DirectMessage).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); } - public Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId) + public Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); return DeleteMessageInternal(guildId, channelId, messageId); } - public Task DeleteDMMessage(ulong channelId, ulong messageId) + public Task DeleteDMMessage(ulong channelId, ulong messageId, RequestOptions options = null) { return DeleteMessageInternal(0, channelId, messageId); } - private async Task DeleteMessageInternal(ulong guildId, ulong channelId, ulong messageId) + private async Task DeleteMessageInternal(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); if (guildId != 0) - await Send("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId).ConfigureAwait(false); + await Send("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId, options: options).ConfigureAwait(false); else - await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false); + await Send("DELETE", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); } - public Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParam args) + public Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); return DeleteMessagesInternal(guildId, channelId, args); } - public Task DeleteDMMessages(ulong channelId, DeleteMessagesParam args) + public Task DeleteDMMessages(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { return DeleteMessagesInternal(0, channelId, args); } - private async Task DeleteMessagesInternal(ulong guildId, ulong channelId, DeleteMessagesParam args) + private async Task DeleteMessagesInternal(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); + + var messageIds = args.MessageIds?.ToArray(); Preconditions.NotNull(args.MessageIds, nameof(args.MessageIds)); + Preconditions.AtMost(messageIds.Length, 100, nameof(messageIds.Length)); - var messageIds = args.MessageIds.ToArray(); switch (messageIds.Length) { case 0: @@ -917,23 +938,23 @@ namespace Discord.API break; default: if (guildId != 0) - await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId).ConfigureAwait(false); + await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId, options: options).ConfigureAwait(false); else - await Send("POST", $"channels/{channelId}/messages/bulk_delete", args).ConfigureAwait(false); + await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, options: options).ConfigureAwait(false); break; } } - public Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) + public Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); return ModifyMessageInternal(guildId, channelId, messageId, args); } - public Task ModifyDMMessage(ulong channelId, ulong messageId, ModifyMessageParams args) + public Task ModifyDMMessage(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { return ModifyMessageInternal(0, channelId, messageId, args); } - private async Task ModifyMessageInternal(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) + private async Task ModifyMessageInternal(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); @@ -946,104 +967,103 @@ namespace Discord.API } if (guildId != 0) - return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); + return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args).ConfigureAwait(false); + return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, options: options).ConfigureAwait(false); } - public async Task AckMessage(ulong channelId, ulong messageId) + public async Task AckMessage(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); - await Send("POST", $"channels/{channelId}/messages/{messageId}/ack").ConfigureAwait(false); + await Send("POST", $"channels/{channelId}/messages/{messageId}/ack", options: options).ConfigureAwait(false); } - public async Task TriggerTypingIndicator(ulong channelId) + public async Task TriggerTypingIndicator(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - await Send("POST", $"channels/{channelId}/typing").ConfigureAwait(false); + await Send("POST", $"channels/{channelId}/typing", options: options).ConfigureAwait(false); } //Users - public async Task GetUser(ulong userId) + public async Task GetUser(ulong userId, RequestOptions options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); try { - return await Send("GET", $"users/{userId}").ConfigureAwait(false); + return await Send("GET", $"users/{userId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetUser(string username, ushort discriminator) + public async Task GetUser(string username, ushort discriminator, RequestOptions options = null) { Preconditions.NotNullOrEmpty(username, nameof(username)); try { - var models = await QueryUsers($"{username}#{discriminator}", 1).ConfigureAwait(false); + var models = await QueryUsers($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); return models.FirstOrDefault(); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> QueryUsers(string query, int limit) + public async Task> QueryUsers(string query, int limit, RequestOptions options = null) { Preconditions.NotNullOrEmpty(query, nameof(query)); Preconditions.AtLeast(limit, 0, nameof(limit)); - return await Send>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}").ConfigureAwait(false); + return await Send>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}", options: options).ConfigureAwait(false); } //Current User/DMs - public async Task GetCurrentUser() + public async Task GetCurrentUser(RequestOptions options = null) { - return await Send("GET", "users/@me").ConfigureAwait(false); + return await Send("GET", "users/@me", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserConnections() + public async Task> GetCurrentUserConnections(RequestOptions options = null) { - return await Send>("GET", "users/@me/connections").ConfigureAwait(false); + return await Send>("GET", "users/@me/connections", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserDMs() + public async Task> GetCurrentUserDMs(RequestOptions options = null) { - return await Send>("GET", "users/@me/channels").ConfigureAwait(false); + return await Send>("GET", "users/@me/channels", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserGuilds() + public async Task> GetCurrentUserGuilds(RequestOptions options = null) { - return await Send>("GET", "users/@me/guilds").ConfigureAwait(false); + return await Send>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); } - public async Task ModifyCurrentUser(ModifyCurrentUserParams args) + public async Task ModifyCurrentUser(ModifyCurrentUserParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNullOrEmpty(args.Email, nameof(args.Email)); Preconditions.NotNullOrEmpty(args.Username, nameof(args.Username)); - return await Send("PATCH", "users/@me", args).ConfigureAwait(false); + return await Send("PATCH", "users/@me", args, options: options).ConfigureAwait(false); } - public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args) + public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEmpty(args.Nickname, nameof(args.Nickname)); - await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args).ConfigureAwait(false); + await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args, options: options).ConfigureAwait(false); } - public async Task CreateDMChannel(CreateDMChannelParams args) + public async Task CreateDMChannel(CreateDMChannelParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.RecipientId, 0, nameof(args.RecipientId)); - return await Send("POST", $"users/@me/channels", args).ConfigureAwait(false); + return await Send("POST", $"users/@me/channels", args, options: options).ConfigureAwait(false); } //Voice Regions - public async Task> GetVoiceRegions() + public async Task> GetVoiceRegions(RequestOptions options = null) { - return await Send>("GET", "voice/regions").ConfigureAwait(false); + return await Send>("GET", "voice/regions", options: options).ConfigureAwait(false); } - public async Task> GetGuildVoiceRegions(ulong guildId) + public async Task> GetGuildVoiceRegions(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/regions").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/regions", options: options).ConfigureAwait(false); } //Helpers diff --git a/src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs b/src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs new file mode 100644 index 000000000..2d91bde7e --- /dev/null +++ b/src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Gateway +{ + public class ExtendedGuild : Guild + { + [JsonProperty("unavailable")] + public bool? Unavailable { get; set; } + [JsonProperty("member_count")] + public int MemberCount { get; set; } + [JsonProperty("large")] + public bool Large { get; set; } + + [JsonProperty("presences")] + public Presence[] Presences { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + [JsonProperty("channels")] + public Channel[] Channels { get; set; } + [JsonProperty("joined_at")] + public DateTime JoinedAt { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GatewayOpCodes.cs b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs index 82fbf51f3..f4d932d90 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCodes.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs @@ -12,13 +12,19 @@ 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. + /// C→S - Used to ensure the guild's voice server is alive. 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 + RequestGuildMembers = 8, + /// S→C - Used to notify the client that their session has expired and cannot be resumed. + InvalidSession = 9, + /// S→C - Used to provide information to the client immediately on connection. + Hello = 10, + /// S→C - Used to reply to a client's heartbeat. + HeartbeatAck = 11 } } diff --git a/src/Discord.Net/API/Gateway/GuildBanEvent.cs b/src/Discord.Net/API/Gateway/GuildBanEvent.cs new file mode 100644 index 000000000..4d55d3c2a --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildBanEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildBanEvent : User + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs new file mode 100644 index 000000000..8e3b1fc37 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildRoleDeleteEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildRoleDeleteEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ReadyEvent.cs b/src/Discord.Net/API/Gateway/ReadyEvent.cs index c0384c100..6e1b05e73 100644 --- a/src/Discord.Net/API/Gateway/ReadyEvent.cs +++ b/src/Discord.Net/API/Gateway/ReadyEvent.cs @@ -23,11 +23,13 @@ namespace Discord.API.Gateway [JsonProperty("read_state")] public ReadState[] ReadStates { get; set; } [JsonProperty("guilds")] - public Guild[] Guilds { get; set; } + public ExtendedGuild[] Guilds { get; set; } [JsonProperty("private_channels")] public Channel[] PrivateChannels { get; set; } [JsonProperty("heartbeat_interval")] public int HeartbeatInterval { get; set; } + [JsonProperty("relationships")] + public Relationship[] Relationships { get; set; } //Ignored [JsonProperty("user_settings")] diff --git a/src/Discord.Net/API/Rest/DeleteMessagesParam.cs b/src/Discord.Net/API/Rest/DeleteMessagesParams.cs similarity index 83% rename from src/Discord.Net/API/Rest/DeleteMessagesParam.cs rename to src/Discord.Net/API/Rest/DeleteMessagesParams.cs index 8a794499c..41c6512e4 100644 --- a/src/Discord.Net/API/Rest/DeleteMessagesParam.cs +++ b/src/Discord.Net/API/Rest/DeleteMessagesParams.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Discord.API.Rest { - public class DeleteMessagesParam + public class DeleteMessagesParams { [JsonProperty("messages")] public IEnumerable MessageIds { get; set; } diff --git a/src/Discord.Net/API/Rest/LoginParams.cs b/src/Discord.Net/API/Rest/LoginParams.cs deleted file mode 100644 index c5d028063..000000000 --- a/src/Discord.Net/API/Rest/LoginParams.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - public class LoginParams - { - [JsonProperty("email")] - public string Email { get; set; } - [JsonProperty("password")] - public string Password { get; set; } - } -} diff --git a/src/Discord.Net/API/Rest/LoginResponse.cs b/src/Discord.Net/API/Rest/LoginResponse.cs deleted file mode 100644 index 2d566612d..000000000 --- a/src/Discord.Net/API/Rest/LoginResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - public class LoginResponse - { - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs index a29a9c8b4..7e9b668ef 100644 --- a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs @@ -1,5 +1,4 @@ -using Discord.Net.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.IO; namespace Discord.API.Rest @@ -8,12 +7,6 @@ namespace Discord.API.Rest { [JsonProperty("username")] public Optional Username { get; set; } - [JsonProperty("email")] - public Optional Email { get; set; } - [JsonProperty("password")] - public Optional Password { get; set; } - [JsonProperty("new_password")] - public Optional NewPassword { get; set; } [JsonProperty("avatar"), Image] public Optional Avatar { get; set; } } diff --git a/src/Discord.Net/Data/DataStoreProvider.cs b/src/Discord.Net/Data/DataStoreProvider.cs new file mode 100644 index 000000000..db6ea3e56 --- /dev/null +++ b/src/Discord.Net/Data/DataStoreProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Data +{ + public delegate DataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount); +} diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DefaultDataStore.cs new file mode 100644 index 000000000..14dbcae31 --- /dev/null +++ b/src/Discord.Net/Data/DefaultDataStore.cs @@ -0,0 +1,90 @@ +using Discord.Extensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Data +{ + public class DefaultDataStore : DataStore + { + private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 + 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 readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _guilds; + private readonly ConcurrentDictionary _users; + + internal override IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); + internal override IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); + internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + + public DefaultDataStore(int guildCount, int dmChannelCount) + { + double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; + double estimatedUsersCount = guildCount * AverageUsersPerGuild; + _channels = new ConcurrentDictionary(1, (int)(estimatedChannelCount * CollectionMultiplier)); + _guilds = new ConcurrentDictionary(1, (int)(guildCount * CollectionMultiplier)); + _users = new ConcurrentDictionary(1, (int)(estimatedUsersCount * CollectionMultiplier)); + } + + internal override ICachedChannel GetChannel(ulong id) + { + ICachedChannel channel; + if (_channels.TryGetValue(id, out channel)) + return channel; + return null; + } + internal override void AddChannel(ICachedChannel channel) + { + _channels[channel.Id] = channel; + } + internal override ICachedChannel RemoveChannel(ulong id) + { + ICachedChannel channel; + if (_channels.TryRemove(id, out channel)) + return channel; + return null; + } + + internal override CachedGuild GetGuild(ulong id) + { + CachedGuild guild; + if (_guilds.TryGetValue(id, out guild)) + return guild; + return null; + } + internal override void AddGuild(CachedGuild guild) + { + _guilds[guild.Id] = guild; + } + internal override CachedGuild RemoveGuild(ulong id) + { + CachedGuild guild; + if (_guilds.TryRemove(id, out guild)) + return guild; + return null; + } + + internal override CachedPublicUser GetUser(ulong id) + { + CachedPublicUser user; + if (_users.TryGetValue(id, out user)) + return user; + return null; + } + internal override CachedPublicUser GetOrAddUser(ulong id, Func userFactory) + { + return _users.GetOrAdd(id, userFactory); + } + internal override CachedPublicUser RemoveUser(ulong id) + { + CachedPublicUser user; + if (_users.TryRemove(id, out user)) + return user; + return null; + } + } +} diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs new file mode 100644 index 000000000..cc849cd94 --- /dev/null +++ b/src/Discord.Net/Data/IDataStore.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Data +{ + public abstract class DataStore + { + internal abstract IReadOnlyCollection Channels { get; } + internal abstract IReadOnlyCollection Guilds { get; } + internal abstract IReadOnlyCollection Users { get; } + + internal abstract ICachedChannel GetChannel(ulong id); + internal abstract void AddChannel(ICachedChannel channel); + internal abstract ICachedChannel RemoveChannel(ulong id); + + internal abstract CachedGuild GetGuild(ulong id); + internal abstract void AddGuild(CachedGuild guild); + internal abstract CachedGuild RemoveGuild(ulong id); + + internal abstract CachedPublicUser GetUser(ulong id); + internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func userFactory); + internal abstract CachedPublicUser RemoveUser(ulong id); + } +} diff --git a/src/Discord.Net/Data/SharedDataStore.cs b/src/Discord.Net/Data/SharedDataStore.cs new file mode 100644 index 000000000..fd3a6554a --- /dev/null +++ b/src/Discord.Net/Data/SharedDataStore.cs @@ -0,0 +1,11 @@ +namespace Discord.Data +{ + //TODO: Implement + //TODO: CachedPublicUser's GuildCount system is not at all multi-writer threadsafe! + //TODO: CachedPublicUser's Update method is not multi-writer threadsafe! + //TODO: Are there other multiwriters across shards? + + /*public class SharedDataStore + { + }*/ +} diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs similarity index 55% rename from src/Discord.Net/Rest/DiscordClient.cs rename to src/Discord.Net/DiscordClient.cs index 89475ca93..0b0d04390 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,43 +1,38 @@ using Discord.API.Rest; +using Discord.Extensions; using Discord.Logging; using Discord.Net; using Discord.Net.Queue; -using Discord.Net.Rest; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace Discord.Rest +namespace Discord { - //TODO: Docstrings - //TODO: Log Internal/External REST Rate Limits, 502s - //TODO: Log Logins/Logouts - public sealed class DiscordClient : IDiscordClient, IDisposable + public class DiscordClient : IDiscordClient { 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 readonly RequestQueue _requestQueue; - private bool _isDisposed; - private SelfUser _currentUser; + internal readonly Logger _discordLogger, _restLogger; + internal readonly SemaphoreSlim _connectionLock; + internal readonly LogManager _log; + internal readonly RequestQueue _requestQueue; + internal bool _isDisposed; + internal SelfUser _currentUser; public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } - - public IRequestQueue RequestQueue => _requestQueue; public DiscordClient(DiscordConfig config = null) { if (config == null) config = new DiscordConfig(); - + _log = new LogManager(config.LogLevel); _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); @@ -49,26 +44,17 @@ namespace Discord.Rest ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } - - public async Task Login(string email, string password) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } + public async Task Login(TokenType tokenType, string token, bool validateToken = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); + await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) + private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) { if (LoginState != LoginState.LoggedOut) await LogoutInternal().ConfigureAwait(false); @@ -76,13 +62,7 @@ namespace Discord.Rest try { - 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).ConfigureAwait(false); if (validateToken) { @@ -96,6 +76,8 @@ namespace Discord.Rest } } + await OnLogin().ConfigureAwait(false); + LoginState = LoginState.LoggedIn; } catch (Exception) @@ -106,6 +88,7 @@ namespace Discord.Rest await LoggedIn.Raise().ConfigureAwait(false); } + protected virtual Task OnLogin() => Task.CompletedTask; public async Task Logout() { @@ -122,6 +105,8 @@ namespace Discord.Rest LoginState = LoginState.LoggingOut; await ApiClient.Logout().ConfigureAwait(false); + + await OnLogout().ConfigureAwait(false); _currentUser = null; @@ -129,14 +114,15 @@ namespace Discord.Rest await LoggedOut.Raise().ConfigureAwait(false); } + protected virtual Task OnLogout() => Task.CompletedTask; - public async Task> GetConnections() + public async Task> GetConnections() { var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); - return models.Select(x => new Connection(x)); + return models.Select(x => new Connection(x)).ToImmutableArray(); } - public async Task GetChannel(ulong id) + public virtual async Task GetChannel(ulong id) { var model = await ApiClient.GetChannel(id).ConfigureAwait(false); if (model != null) @@ -151,17 +137,17 @@ namespace Discord.Rest } } else - return new DMChannel(this, model); + return new DMChannel(this, new User(this, model.Recipient), model); } return null; } - public async Task> GetDMChannels() + public virtual async Task> GetDMChannels() { var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); - return models.Select(x => new DMChannel(this, x)); + return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); } - public async Task GetInvite(string inviteIdOrXkcd) + public virtual async Task GetInvite(string inviteIdOrXkcd) { var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); if (model != null) @@ -169,48 +155,48 @@ namespace Discord.Rest return null; } - public async Task GetGuild(ulong id) + public virtual async Task GetGuild(ulong id) { var model = await ApiClient.GetGuild(id).ConfigureAwait(false); if (model != null) return new Guild(this, model); return null; } - public async Task GetGuildEmbed(ulong id) + public virtual async Task GetGuildEmbed(ulong id) { var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); if (model != null) return new GuildEmbed(model); return null; } - public async Task> GetGuilds() + public virtual async Task> GetGuilds() { var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); - return models.Select(x => new UserGuild(this, x)); + return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); } - public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) + public virtual async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); return new Guild(this, model); } - public async Task GetUser(ulong id) + public virtual async Task GetUser(ulong id) { var model = await ApiClient.GetUser(id).ConfigureAwait(false); if (model != null) - return new PublicUser(this, model); + return new User(this, model); return null; } - public async Task GetUser(string username, ushort discriminator) + public virtual async Task GetUser(string username, ushort discriminator) { var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); if (model != null) - return new PublicUser(this, model); + return new User(this, model); return null; } - public async Task GetCurrentUser() + public virtual async Task GetCurrentUser() { var user = _currentUser; if (user == null) @@ -221,60 +207,32 @@ namespace Discord.Rest } return user; } - public async Task> QueryUsers(string query, int limit) + public virtual async Task> QueryUsers(string query, int limit) { var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); - return models.Select(x => new PublicUser(this, x)); + return models.Select(x => new User(this, x)).ToImmutableArray(); } - public async Task> GetVoiceRegions() + public virtual async Task> GetVoiceRegions() { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); - return models.Select(x => new VoiceRegion(x)); + return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); } - public async Task GetVoiceRegion(string id) + public virtual async Task GetVoiceRegion(string id) { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); } - void Dispose(bool disposing) + internal void Dispose(bool disposing) { if (!_isDisposed) _isDisposed = true; } public void Dispose() => Dispose(true); - + 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() - => await GetDMChannels().ConfigureAwait(false); - async Task> IDiscordClient.GetConnections() - => await GetConnections().ConfigureAwait(false); - async Task IDiscordClient.GetInvite(string inviteIdOrXkcd) - => await GetInvite(inviteIdOrXkcd).ConfigureAwait(false); - async Task IDiscordClient.GetGuild(ulong id) - => await GetGuild(id).ConfigureAwait(false); - async Task> IDiscordClient.GetGuilds() - => await GetGuilds().ConfigureAwait(false); - async Task IDiscordClient.CreateGuild(string name, IVoiceRegion region, Stream jpegIcon) - => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); - 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); - async Task IDiscordClient.GetCurrentUser() - => await GetCurrentUser().ConfigureAwait(false); - async Task> IDiscordClient.QueryUsers(string query, int limit) - => await QueryUsers(query, limit).ConfigureAwait(false); - async Task> IDiscordClient.GetVoiceRegions() - => await GetVoiceRegions().ConfigureAwait(false); - async Task IDiscordClient.GetVoiceRegion(string id) - => await GetVoiceRegion(id).ConfigureAwait(false); + Task IDiscordClient.Connect() { throw new NotSupportedException(); } + Task IDiscordClient.Disconnect() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 35e6f010b..54989ec63 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -10,7 +10,7 @@ namespace Discord public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - public const int GatewayAPIVersion = 3; //TODO: Upgrade to 4 + public const int GatewayAPIVersion = 5; public const string GatewayEncoding = "json"; public const string ClientAPIUrl = "https://discordapp.com/api/"; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs new file mode 100644 index 000000000..af4551872 --- /dev/null +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -0,0 +1,708 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Data; +using Discord.Extensions; +using Discord.Logging; +using Discord.Net.Converters; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + //TODO: Remove unnecessary `as` casts + //TODO: Add docstrings + public class DiscordSocketClient : DiscordClient, IDiscordClient + { + public event Func Connected, Disconnected; + public event Func Ready; + //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 CurrentUserUpdated; + public event Func UserIsTyping;*/ + + private readonly ConcurrentQueue _largeGuilds; + private readonly Logger _gatewayLogger; + private readonly DataStoreProvider _dataStoreProvider; + private readonly JsonSerializer _serializer; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + private readonly bool _enablePreUpdateEvents; + private readonly int _largeThreshold; + private readonly int _totalShards; + private ImmutableDictionary _voiceRegions; + private string _sessionId; + + public int ShardId { get; } + public ConnectionState ConnectionState { get; private set; } + public IWebSocketClient GatewaySocket { get; private set; } + internal int MessageCacheSize { get; private set; } + //internal bool UsePermissionCache { get; private set; } + internal DataStore DataStore { get; private set; } + + internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; + internal IReadOnlyCollection Guilds + { + get + { + var guilds = DataStore.Guilds; + return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds); + } + } + internal IReadOnlyCollection DMChannels + { + get + { + var users = DataStore.Users; + return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users); + } + } + internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + + public DiscordSocketClient(DiscordSocketConfig config = null) + { + if (config == null) + config = new DiscordSocketConfig(); + + ShardId = config.ShardId; + _totalShards = config.TotalShards; + + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; + _dataStoreProvider = config.DataStoreProvider; + + MessageCacheSize = config.MessageCacheSize; + //UsePermissionCache = config.UsePermissionsCache; + _enablePreUpdateEvents = config.EnablePreUpdateEvents; + _largeThreshold = config.LargeThreshold; + + _gatewayLogger = _log.CreateLogger("Gateway"); + + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}"); + ApiClient.ReceivedGatewayEvent += ProcessMessage; + GatewaySocket = config.WebSocketProvider(); + + _voiceRegions = ImmutableDictionary.Create(); + _largeGuilds = new ConcurrentQueue(); + } + + protected override async Task OnLogin() + { + var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); + } + protected override async Task OnLogout() + { + if (ConnectionState != ConnectionState.Disconnected) + await DisconnectInternal().ConfigureAwait(false); + + _voiceRegions = ImmutableDictionary.Create(); + } + + public async Task Connect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternal() + { + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting."); + + ConnectionState = ConnectionState.Connecting; + try + { + await ApiClient.Connect().ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternal().ConfigureAwait(false); + throw; + } + + await Connected.Raise().ConfigureAwait(false); + } + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + 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 override Task GetVoiceRegion(string id) + { + VoiceRegion region; + if (_voiceRegions.TryGetValue(id, out region)) + return Task.FromResult(region); + return Task.FromResult(null); + } + + public override Task GetGuild(ulong id) + { + return Task.FromResult(DataStore.GetGuild(id)); + } + internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null) + { + var guild = new CachedGuild(this, model); + for (int i = 0; i < model.Channels.Length; i++) + AddCachedChannel(model.Channels[i], dataStore); + DataStore.AddGuild(guild); + if (model.Large) + _largeGuilds.Enqueue(model.Id); + return guild; + } + internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null) + { + var guild = DataStore.RemoveGuild(id) as CachedGuild; + foreach (var channel in guild.Channels) + guild.RemoveCachedChannel(channel.Id); + foreach (var user in guild.Members) + guild.RemoveCachedUser(user.Id); + return guild; + } + internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id) as CachedGuild; + + public override Task GetChannel(ulong id) + { + return Task.FromResult(DataStore.GetChannel(id)); + } + internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) + { + if (model.IsPrivate) + { + var recipient = AddCachedUser(model.Recipient); + return recipient.SetDMChannel(model); + } + else + { + var guild = GetCachedGuild(model.GuildId.Value); + return guild.AddCachedChannel(model); + } + } + internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null) + { + var channel = DataStore.RemoveChannel(id) as ICachedChannel; + var dmChannel = channel as CachedDMChannel; + if (dmChannel != null) + { + var recipient = dmChannel.Recipient; + recipient.RemoveDMChannel(id); + } + return channel; + } + internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id) as ICachedChannel; + + public override Task GetUser(ulong id) + { + return Task.FromResult(DataStore.GetUser(id)); + } + public override Task GetUser(string username, ushort discriminator) + { + return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); + } + internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) + { + var user = DataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; + user.AddRef(); + return user; + } + internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null) + { + var user = DataStore.GetUser(id) as CachedPublicUser; + user.RemoveRef(); + return user; + } + + private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload) + { + try + { + switch (opCode) + { + case GatewayOpCodes.Dispatch: + switch (type) + { + //Global + case "READY": + { + //TODO: Make downloading large guilds optional + var data = payload.ToObject(_serializer); + var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + + _currentUser = new CachedSelfUser(this,data.User); + + for (int i = 0; i < data.Guilds.Length; i++) + AddCachedGuild(data.Guilds[i], dataStore); + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddCachedChannel(data.PrivateChannels[i], dataStore); + + _sessionId = data.SessionId; + DataStore = dataStore; + + await Ready().ConfigureAwait(false); + } + break; + + //Guilds + /*case "GUILD_CREATE": + { + var data = payload.ToObject(_serializer); + var guild = new CachedGuild(this, data); + DataStore.AddGuild(guild); + + if (data.Unavailable == false) + type = "GUILD_AVAILABLE"; + else + await JoinedGuild.Raise(guild).ConfigureAwait(false); + + if (!data.Large) + await GuildAvailable.Raise(guild); + else + _largeGuilds.Enqueue(data.Id); + } + break; + case "GUILD_UPDATE": + { + var data = payload.ToObject(_serializer); + var guild = DataStore.GetGuild(data.Id); + if (guild != null) + { + var before = _enablePreUpdateEvents ? guild.Clone() : null; + guild.Update(data); + await GuildUpdated.Raise(before, guild); + } + else + await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); + } + break; + case "GUILD_DELETE": + { + var data = payload.ToObject(_serializer); + var guild = DataStore.RemoveGuild(data.Id); + if (guild != null) + { + if (data.Unavailable == true) + type = "GUILD_UNAVAILABLE"; + + await GuildUnavailable.Raise(guild); + if (data.Unavailable != true) + await LeftGuild.Raise(guild); + } + else + await _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild."); + } + break; + + //Channels + case "CHANNEL_CREATE": + { + var data = payload.ToObject(_serializer); + + IChannel channel = null; + if (data.GuildId != null) + { + var guild = GetCachedGuild(data.GuildId.Value); + if (guild != null) + channel = guild.AddCachedChannel(data.Id, true); + else + await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + } + else + channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id); + if (channel != null) + { + channel.Update(data); + await ChannelCreated.Raise(channel); + } + } + break; + case "CHANNEL_UPDATE": + { + var data = payload.ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id) as Channel; + if (channel != null) + { + var before = _enablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data); + await ChannelUpdated.Raise(before, channel); + } + else + await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); + } + break; + case "CHANNEL_DELETE": + { + var data = payload.ToObject(_serializer); + var channel = RemoveCachedChannel(data.Id); + if (channel != null) + await ChannelDestroyed.Raise(channel); + else + await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); + } + break; + + //Members + case "GUILD_MEMBER_ADD": + { + var data = payload.ToObject(_serializer); + var guild = GetGuild(data.GuildId.Value); + if (guild != null) + { + var user = guild.AddCachedUser(data.User.Id, true, true); + user.Update(data); + user.UpdateActivity(); + UserJoined.Raise(user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); + } + break; + case "GUILD_MEMBER_UPDATE": + { + var data = payload.ToObject(_serializer); + var guild = GetGuild(data.GuildId.Value); + if (guild != null) + { + var user = guild.GetCachedUser(data.User.Id); + if (user != null) + { + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + await UserUpdated.Raise(before, user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); + } + break; + case "GUILD_MEMBER_REMOVE": + { + var data = payload.ToObject(_serializer); + var guild = GetGuild(data.GuildId.Value); + if (guild != null) + { + var user = guild.RemoveCachedUser(data.User.Id); + if (user != null) + { + user.GlobalUser.RemoveGuild(); + if (user.GuildCount == 0 && user.DMChannel == null) + DataStore.RemoveUser(user.Id); + await UserLeft.Raise(user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); + } + break; + case "GUILD_MEMBERS_CHUNK": + { + var data = payload.ToObject(_serializer); + var guild = GetCachedGuild(data.GuildId); + if (guild != null) + { + foreach (var memberData in data.Members) + { + var user = guild.AddCachedUser(memberData.User.Id, true, false); + user.Update(memberData); + } + + if (guild.CurrentUserCount >= guild.UserCount) //Finished downloading for there + await GuildAvailable.Raise(guild); + } + else + await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); + } + break; + + //Roles + case "GUILD_ROLE_CREATE": + { + var data = payload.ToObject(_serializer); + var guild = GetCachedGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddCachedRole(data.Data.Id); + role.Update(data.Data, false); + RoleCreated.Raise(role); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); + } + break; + case "GUILD_ROLE_UPDATE": + { + var data = payload.ToObject(_serializer); + var guild = GetCachedGuild(data.GuildId); + if (guild != null) + { + var role = guild.GetRole(data.Data.Id); + if (role != null) + { + var before = _enablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Data, true); + RoleUpdated.Raise(before, role); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); + } + break; + case "GUILD_ROLE_DELETE": + { + var data = payload.ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId) as CachedGuild; + if (guild != null) + { + var role = guild.RemoveRole(data.RoleId); + if (role != null) + RoleDeleted.Raise(role); + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); + } + break; + + //Bans + case "GUILD_BAN_ADD": + { + var data = payload.ToObject(_serializer); + var guild = GetCachedGuild(data.GuildId); + if (guild != null) + await UserBanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); + } + break; + case "GUILD_BAN_REMOVE": + { + var data = payload.ToObject(_serializer); + var guild = GetCachedGuild(data.GuildId); + if (guild != null) + await UserUnbanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); + } + break; + + //Messages + case "MESSAGE_CREATE": + { + var data = payload.ToObject(_serializer); + + var channel = DataStore.GetChannel(data.ChannelId); + if (channel != null) + { + var user = channel.GetUser(data.Author.Id); + + if (user != null) + { + bool isAuthor = data.Author.Id == CurrentUser.Id; + var msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); + + msg.Update(data); + + MessageReceived.Raise(msg); + } + else + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + } + else + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); + } + break; + case "MESSAGE_UPDATE": + { + var data = payload.ToObject(_serializer); + var channel = GetCachedChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.GetMessage(data.Id, data.Author?.Id); + var before = _enablePreUpdateEvents ? msg.Clone() : null; + msg.Update(data); + MessageUpdated.Raise(before, msg); + } + else + await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); + } + break; + case "MESSAGE_DELETE": + { + var data = payload.ToObject(_serializer); + var channel = GetCachedChannel(data.ChannelId); + if (channel != null) + { + var msg = channel.RemoveMessage(data.Id); + MessageDeleted.Raise(msg); + } + else + await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); + } + break; + + //Statuses + case "PRESENCE_UPDATE": + { + var data = payload.ToObject(_serializer); + User user; + Guild guild; + if (data.GuildId == null) + { + guild = null; + user = GetPrivateChannel(data.User.Id)?.Recipient; + } + else + { + guild = GetGuild(data.GuildId.Value); + if (guild == null) + { + await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); + break; + } + else + user = guild.GetUser(data.User.Id); + } + + if (user != null) + { + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + UserUpdated.Raise(before, user); + } + else + { + //Occurs when a user leaves a guild + //await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user."); + } + } + break; + case "TYPING_START": + { + var data = payload.ToObject(_serializer); + var channel = GetCachedChannel(data.ChannelId); + if (channel != null) + { + var user = channel.GetUser(data.UserId); + if (user != null) + { + await UserIsTyping.Raise(channel, user); + user.UpdateActivity(); + } + } + else + await _gatewayLogger.Warning("TYPING_START referenced an unknown channel."); + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + var data = payload.ToObject(_serializer); + var guild = GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.GetUser(data.UserId); + if (user != null) + { + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data); + UserUpdated.Raise(before, user); + } + else + { + //Occurs when a user leaves a guild + //await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); + } + } + else + await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild."); + } + break; + + //Settings + case "USER_UPDATE": + { + var data = payload.ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data); + await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + } + } + 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 + case "RESUMED": //TODO: Add + await _gatewayLogger.Debug($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); + return; + + //Others + default: + await _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); + return; + } + break; + } + } + catch (Exception ex) + { + await _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + return; + } + await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs similarity index 88% rename from src/Discord.Net/WebSocket/DiscordSocketConfig.cs rename to src/Discord.Net/DiscordSocketConfig.cs index 4318bd247..760f9818c 100644 --- a/src/Discord.Net/WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -1,7 +1,7 @@ -using Discord.Net.WebSockets; -using Discord.WebSocket.Data; +using Discord.Data; +using Discord.Net.WebSockets; -namespace Discord.WebSocket +namespace Discord { public class DiscordSocketConfig : DiscordConfig { @@ -15,15 +15,15 @@ namespace Discord.WebSocket /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. public int ReconnectDelay { get; set; } = 1000; /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; + public int FailedReconnectDelay { get; set; } = 15000; /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheSize { get; set; } = 100; - /// + /*/// /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. + /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage. /// - public bool UsePermissionsCache { get; set; } = true; + public bool UsePermissionsCache { get; set; } = false;*/ /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. public bool EnablePreUpdateEvents { get; set; } = true; /// diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs new file mode 100644 index 000000000..eaaf3b8e6 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -0,0 +1,126 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class DMChannel : SnowflakeEntity, IDMChannel + { + public override DiscordClient Discord { get; } + public User Recipient { get; private set; } + + public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); + + public DMChannel(DiscordClient discord, User recipient, Model model) + : base(model.Id) + { + Discord = discord; + Recipient = recipient; + + Update(model, UpdateSource.Creation); + } + protected void Update(Model model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + Recipient.Update(model.Recipient, UpdateSource.Rest); + } + + public async Task Update() + { + if (IsAttached) throw new NotSupportedException(); + + var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); + Update(model, UpdateSource.Rest); + } + public async Task Close() + { + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); + } + + public virtual async Task GetUser(ulong id) + { + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + if (id == Recipient.Id) + return Recipient; + else if (id == currentUser.Id) + return currentUser; + else + return null; + } + public virtual async Task> GetUsers() + { + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + return ImmutableArray.Create(currentUser, Recipient); + } + public virtual async Task> GetUsers(int limit, int offset) + { + var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + return new IUser[] { currentUser, Recipient }.Skip(offset).Take(limit).ToImmutableArray(); + } + + public async Task SendMessage(string text, bool isTTS) + { + var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + public async Task SendFile(string filePath, string text, bool isTTS) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + { + var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + } + public async Task SendFile(Stream stream, string filename, string text, bool isTTS) + { + var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + public virtual async Task GetMessage(ulong id) + { + var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); + if (model != null) + return new Message(this, new User(Discord, model.Author), model); + return null; + } + public virtual async Task> GetMessages(int limit) + { + var args = new GetChannelMessagesParams { Limit = limit }; + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + } + public virtual async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + { + var args = new GetChannelMessagesParams { Limit = limit }; + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + } + public async Task DeleteMessages(IEnumerable messages) + { + await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + } + + public async Task TriggerTyping() + { + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + } + + public override string ToString() => '@' + Recipient.ToString(); + private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; + + IUser IDMChannel.Recipient => Recipient; + IMessage IMessageChannel.GetCachedMessage(ulong id) => null; + } +} diff --git a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs similarity index 57% rename from src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs rename to src/Discord.Net/Entities/Channels/GuildChannel.cs index 66e0abe19..0cdff457a 100644 --- a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -1,42 +1,39 @@ using Discord.API.Rest; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; -namespace Discord.Rest +namespace Discord { - public abstract class GuildChannel : IGuildChannel + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel { private ConcurrentDictionary _overwrites; - /// - public ulong Id { get; } - /// Gets the guild this channel is a member of. - public Guild Guild { get; } - - /// public string Name { get; private set; } - /// public int Position { get; private set; } - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// - public IReadOnlyDictionary PermissionOverwrites => _overwrites; - internal DiscordClient Discord => Guild.Discord; + public Guild Guild { get; private set; } + + public override DiscordClient Discord => Guild.Discord; - internal GuildChannel(Guild guild, Model model) + public GuildChannel(Guild guild, Model model) + : base(model.Id) { - Id = model.Id; Guild = guild; - Update(model); + Update(model, UpdateSource.Creation); } - internal virtual void Update(Model model) + protected virtual void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + Name = model.Name; Position = model.Position; @@ -49,6 +46,13 @@ namespace Discord.Rest _overwrites = newOverwrites; } + public async Task Update() + { + if (IsAttached) throw new NotSupportedException(); + + var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); + Update(model, UpdateSource.Rest); + } public async Task Modify(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); @@ -56,10 +60,35 @@ namespace Discord.Rest var args = new ModifyGuildChannelParams(); func(args); var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } - - /// + public async Task Delete() + { + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); + } + + public abstract Task GetUser(ulong id); + public abstract Task> GetUsers(); + public abstract Task> GetUsers(int limit, int offset); + + public async Task> GetInvites() + { + var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); + return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); + } + public async Task CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) + { + var args = new CreateChannelInviteParams + { + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + Temporary = isTemporary, + XkcdPass = withXkcd + }; + var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); + return new InviteMetadata(Discord, model); + } + public OverwritePermissions? GetPermissionOverwrite(IUser user) { Overwrite value; @@ -67,7 +96,6 @@ namespace Discord.Rest return value.Permissions; return null; } - /// public OverwritePermissions? GetPermissionOverwrite(IRole role) { Overwrite value; @@ -75,28 +103,19 @@ namespace Discord.Rest return value.Permissions; return null; } - /// Downloads a collection of all invites to this channel. - public async Task> GetInvites() - { - var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)); - } - - /// + public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); } - /// public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); } - /// public async Task RemovePermissionOverwrite(IUser user) { await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); @@ -104,7 +123,6 @@ namespace Discord.Rest Overwrite value; _overwrites.TryRemove(user.Id, out value); } - /// public async Task RemovePermissionOverwrite(IRole role) { await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); @@ -112,58 +130,15 @@ namespace Discord.Rest Overwrite value; _overwrites.TryRemove(role.Id, out value); } - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the guild after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) - { - var args = new CreateChannelInviteParams - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - Temporary = isTemporary, - XkcdPass = withXkcd - }; - var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); - return new InviteMetadata(Discord, model); - } - - /// - public async Task Delete() - { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); - } - /// - public async Task Update() - { - var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); - Update(model); - } - - /// + public override string ToString() => Name; - - protected abstract Task GetUserInternal(ulong id); - protected abstract Task> GetUsersInternal(); - protected abstract Task> GetUsersInternal(int limit, int offset); - + private string DebuggerDisplay => $"{Name} ({Id})"; + IGuild IGuildChannel.Guild => Guild; - async Task IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) - => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); - async Task> IGuildChannel.GetInvites() - => await GetInvites().ConfigureAwait(false); - async Task> IGuildChannel.GetUsers() - => await GetUsersInternal().ConfigureAwait(false); - async Task> IChannel.GetUsers() - => await GetUsersInternal().ConfigureAwait(false); - async Task> IChannel.GetUsers(int limit, int offset) - => await GetUsersInternal(limit, offset).ConfigureAwait(false); - async Task IGuildChannel.GetUser(ulong id) - => await GetUserInternal(id).ConfigureAwait(false); - async Task IChannel.GetUser(ulong id) - => await GetUserInternal(id).ConfigureAwait(false); + IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); + + async Task IChannel.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false); + async Task> IChannel.GetUsers() => await GetUsers().ConfigureAwait(false); + async Task> IChannel.GetUsers(int limit, int offset) => await GetUsers(limit, offset).ConfigureAwait(false); } } diff --git a/src/Discord.Net/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs index 74ab4a2f2..13040dc78 100644 --- a/src/Discord.Net/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -6,9 +6,9 @@ namespace Discord public interface IChannel : ISnowflakeEntity { /// Gets a collection of all users in this channel. - Task> GetUsers(); + Task> GetUsers(); /// Gets a paginated collection of all users in this channel. - Task> GetUsers(int limit, int offset = 0); + Task> GetUsers(int limit, int offset = 0); /// Gets a user in this channel with the provided id. Task GetUser(ulong id); } diff --git a/src/Discord.Net/Entities/Channels/IGuildChannel.cs b/src/Discord.Net/Entities/Channels/IGuildChannel.cs index 0a6cf2f1b..81d4a0f2e 100644 --- a/src/Discord.Net/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/IGuildChannel.cs @@ -22,11 +22,11 @@ namespace Discord /// If true, creates a human-readable link. Not supported if maxAge is set to null. Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); /// Returns a collection of all invites to this channel. - Task> GetInvites(); + Task> GetInvites(); /// Gets a collection of permission overwrites for this channel. - IReadOnlyDictionary PermissionOverwrites { get; } - + IReadOnlyCollection PermissionOverwrites { get; } + /// Modifies this guild channel. Task Modify(Action func); @@ -44,7 +44,7 @@ namespace Discord Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions); /// Gets a collection of all users in this channel. - new Task> GetUsers(); + new Task> GetUsers(); /// Gets a user in this channel with the provided id. new Task GetUser(ulong id); } diff --git a/src/Discord.Net/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs index e0613da48..bb9015c1f 100644 --- a/src/Discord.Net/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -7,25 +7,25 @@ namespace Discord public interface IMessageChannel : IChannel { /// Gets all messages in this channel's cache. - IEnumerable CachedMessages { get; } + IReadOnlyCollection CachedMessages { get; } - /// Gets the message from this channel's cache with the given id, or null if none was found. - Task GetCachedMessage(ulong id); - - /// Gets the last N messages from this message channel. - Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. - Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - - /// Sends a message to this text channel. + /// Sends a message to this message channel. Task SendMessage(string text, bool isTTS = false); /// Sends a file to this text channel, with an optional caption. Task SendFile(string filePath, string text = null, bool isTTS = false); /// Sends a file to this text channel, with an optional caption. Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false); - + /// Gets a message from this message channel with the given id, or null if not found. + Task GetMessage(ulong id); + /// Gets the message from this channel's cache with the given id, or null if not found. + IMessage GetCachedMessage(ulong id); + /// Gets the last N messages from this message channel. + Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch); + /// Gets a collection of messages in this channel. + Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); /// Bulk deletes multiple messages. Task DeleteMessages(IEnumerable messages); + /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. Task TriggerTyping(); diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs new file mode 100644 index 000000000..17346d3e6 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -0,0 +1,116 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class TextChannel : GuildChannel, ITextChannel + { + public string Topic { get; private set; } + + public string Mention => MentionUtils.Mention(this); + public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); + + public TextChannel(Guild guild, Model model) + : base(guild, model) + { + } + protected override void Update(Model model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + Topic = model.Topic; + base.Update(model, UpdateSource.Rest); + } + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var args = new ModifyTextChannelParams(); + func(args); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + Update(model, UpdateSource.Rest); + } + + public override async Task GetUser(ulong id) + { + var user = await Guild.GetUser(id).ConfigureAwait(false); + if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) + return user; + return null; + } + public override async Task> GetUsers() + { + var users = await Guild.GetUsers().ConfigureAwait(false); + return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); + } + public override async Task> GetUsers(int limit, int offset) + { + var users = await Guild.GetUsers(limit, offset).ConfigureAwait(false); + return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); + } + + public async Task SendMessage(string text, bool isTTS) + { + var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + public async Task SendFile(string filePath, string text, bool isTTS) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + { + var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + } + public async Task SendFile(Stream stream, string filename, string text, bool isTTS) + { + var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); + return new Message(this, new User(Discord, model.Author), model); + } + public virtual async Task GetMessage(ulong id) + { + var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); + if (model != null) + return new Message(this, new User(Discord, model.Author), model); + return null; + } + public virtual async Task> GetMessages(int limit) + { + var args = new GetChannelMessagesParams { Limit = limit }; + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + } + public virtual async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + { + var args = new GetChannelMessagesParams { Limit = limit }; + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + } + public async Task DeleteMessages(IEnumerable messages) + { + await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + } + + public async Task TriggerTyping() + { + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + } + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + + IMessage IMessageChannel.GetCachedMessage(ulong id) => null; + } +} diff --git a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs similarity index 52% rename from src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs rename to src/Discord.Net/Entities/Channels/VoiceChannel.cs index e105aabd6..fd63ada44 100644 --- a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -5,28 +5,27 @@ using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Channel; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class VoiceChannel : GuildChannel, IVoiceChannel + internal class VoiceChannel : GuildChannel, IVoiceChannel { - /// public int Bitrate { get; private set; } - /// public int UserLimit { get; private set; } - internal VoiceChannel(Guild guild, Model model) + public VoiceChannel(Guild guild, Model model) : base(guild, model) { } - internal override void Update(Model model) + protected override void Update(Model model, UpdateSource source) { - base.Update(model); + if (source == UpdateSource.Rest && IsAttached) return; + + base.Update(model, UpdateSource.Rest); Bitrate = model.Bitrate; UserLimit = model.UserLimit; } - - /// + public async Task Modify(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); @@ -34,12 +33,21 @@ namespace Discord.Rest var args = new ModifyVoiceChannelParams(); func(args); var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } - protected override Task GetUserInternal(ulong id) { throw new NotSupportedException(); } - protected override Task> GetUsersInternal() { throw new NotSupportedException(); } - protected override Task> GetUsersInternal(int limit, int offset) { throw new NotSupportedException(); } + public override Task GetUser(ulong id) + { + throw new NotSupportedException(); + } + public override Task> GetUsers() + { + throw new NotSupportedException(); + } + public override Task> GetUsers(int limit, int offset) + { + throw new NotSupportedException(); + } private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; } diff --git a/src/Discord.Net/Entities/Entity.cs b/src/Discord.Net/Entities/Entity.cs new file mode 100644 index 000000000..4ffd45d1f --- /dev/null +++ b/src/Discord.Net/Entities/Entity.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + internal abstract class Entity : IEntity + { + public T Id { get; } + + public abstract DiscordClient Discord { get; } + + public bool IsAttached => this is ICachedEntity; + + public Entity(T id) + { + Id = id; + } + } +} diff --git a/src/Discord.Net/Entities/Guilds/Emoji.cs b/src/Discord.Net/Entities/Guilds/Emoji.cs index 9f4478e74..55ca2ede6 100644 --- a/src/Discord.Net/Entities/Guilds/Emoji.cs +++ b/src/Discord.Net/Entities/Guilds/Emoji.cs @@ -11,7 +11,7 @@ namespace Discord public bool RequireColons { get; } public IImmutableList RoleIds { get; } - internal Emoji(Model model) + public Emoji(Model model) { Id = model.Id; Name = model.Name; diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs similarity index 58% rename from src/Discord.Net/Rest/Entities/Guilds/Guild.cs rename to src/Discord.Net/Entities/Guilds/Guild.cs index 936a0d35c..dfd0a041e 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -1,77 +1,60 @@ using Discord.API.Rest; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Guild; using EmbedModel = Discord.API.GuildEmbed; +using Model = Discord.API.Guild; using RoleModel = Discord.API.Role; -using System.Diagnostics; -namespace Discord.Rest +namespace Discord { - /// Represents a Discord guild (called a server in the official client). [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Guild : IGuild + internal class Guild : SnowflakeEntity, IGuild { - private ConcurrentDictionary _roles; - private string _iconId, _splashId; - - /// - public ulong Id { get; } - internal DiscordClient Discord { get; } - - /// + protected ConcurrentDictionary _roles; + protected string _iconId, _splashId; + public string Name { get; private set; } - /// public int AFKTimeout { get; private set; } - /// public bool IsEmbeddable { get; private set; } - /// public int VerificationLevel { get; private set; } - /// public ulong? AFKChannelId { get; private set; } - /// public ulong? EmbedChannelId { get; private set; } - /// public ulong OwnerId { get; private set; } - /// public string VoiceRegionId { get; private set; } - /// - public IReadOnlyList Emojis { get; private set; } - /// - public IReadOnlyList Features { get; private set; } + public override DiscordClient Discord { get; } + public ImmutableArray Emojis { get; protected set; } + public ImmutableArray Features { get; protected set; } - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// + public ulong DefaultChannelId => Id; public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - /// public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); - /// - public ulong DefaultChannelId => Id; - /// + public Role EveryoneRole => GetRole(Id); - /// Gets a collection of all roles in this guild. - public IEnumerable Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty(); + public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); - internal Guild(DiscordClient discord, Model model) + public Guild(DiscordClient discord, Model model) + : base(model.Id) { - Id = model.Id; Discord = discord; - Update(model); + Update(model, UpdateSource.Creation); } - private void Update(Model model) + public void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + AFKChannelId = model.AFKChannelId; - AFKTimeout = model.AFKTimeout; EmbedChannelId = model.EmbedChannelId; + AFKTimeout = model.AFKTimeout; IsEmbeddable = model.EmbedEnabled; - Features = model.Features; + Features = model.Features.ToImmutableArray(); _iconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; @@ -84,10 +67,10 @@ namespace Discord.Rest var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emojis.Add(new Emoji(model.Emojis[i])); - Emojis = emojis.ToArray(); + Emojis = emojis.ToImmutableArray(); } else - Emojis = Array.Empty(); + Emojis = ImmutableArray.Create(); var roles = new ConcurrentDictionary(1, model.Roles?.Length ?? 0); if (model.Roles != null) @@ -97,28 +80,32 @@ namespace Discord.Rest } _roles = roles; } - private void Update(EmbedModel model) + public void Update(EmbedModel model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + IsEmbeddable = model.Enabled; EmbedChannelId = model.ChannelId; } - private void Update(IEnumerable models) + public void Update(IEnumerable models, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + Role role; foreach (var model in models) { if (_roles.TryGetValue(model.Id, out role)) - role.Update(model); + role.Update(model, UpdateSource.Rest); } } - - /// + public async Task Update() { + if (IsAttached) throw new NotSupportedException(); + var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); - Update(response); + Update(response, UpdateSource.Rest); } - /// public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -126,9 +113,8 @@ namespace Discord.Rest var args = new ModifyGuildParams(); func(args); var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } - /// public async Task ModifyEmbed(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -136,68 +122,57 @@ namespace Discord.Rest var args = new ModifyGuildEmbedParams(); func(args); var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } - /// public async Task ModifyChannels(IEnumerable args) { + //TODO: Update channels await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); } - /// public async Task ModifyRoles(IEnumerable args) { var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); - Update(models); + Update(models, UpdateSource.Rest); } - /// public async Task Leave() { await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } - /// public async Task Delete() { await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } - - /// - public async Task> GetBans() + + public async Task> GetBans() { var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); - return models.Select(x => new PublicUser(Discord, x)); + return models.Select(x => new User(Discord, x)).ToImmutableArray(); } - /// public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); - /// public async Task AddBan(ulong userId, int pruneDays = 0) { var args = new CreateGuildBanParams() { PruneDays = pruneDays }; await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); } - /// public Task RemoveBan(IUser user) => RemoveBan(user.Id); - /// public async Task RemoveBan(ulong userId) { await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); } - - /// Gets the channel in this guild with the provided id, or null if not found. - public async Task GetChannel(ulong id) + + public virtual async Task GetChannel(ulong id) { var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); if (model != null) return ToChannel(model); return null; } - /// Gets a collection of all channels in this guild. - public async Task> GetChannels() + public virtual async Task> GetChannels() { var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); - return models.Select(x => ToChannel(x)); + return models.Select(x => ToChannel(x)).ToImmutableArray(); } - /// Creates a new text channel. - public async Task CreateTextChannel(string name) + public async Task CreateTextChannel(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -205,8 +180,7 @@ namespace Discord.Rest var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new TextChannel(this, model); } - /// Creates a new voice channel. - public async Task CreateVoiceChannel(string name) + public async Task CreateVoiceChannel(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -214,29 +188,25 @@ namespace Discord.Rest var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new VoiceChannel(this, model); } - - /// Gets a collection of all integrations attached to this guild. - public async Task> GetIntegrations() + + public async Task> GetIntegrations() { var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); - return models.Select(x => new GuildIntegration(this, x)); + return models.Select(x => new GuildIntegration(this, x)).ToImmutableArray(); } - /// Creates a new integration for this guild. - public async Task CreateIntegration(ulong id, string type) + public async Task CreateIntegration(ulong id, string type) { var args = new CreateGuildIntegrationParams { Id = id, Type = type }; var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); return new GuildIntegration(this, model); } - - /// Gets a collection of all invites to this guild. - public async Task> GetInvites() + + public async Task> GetInvites() { var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)); + return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); } - /// Creates a new invite to this guild. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) + public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) { if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); @@ -251,18 +221,15 @@ namespace Discord.Rest var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } - - /// Gets the role in this guild with the provided id, or null if not found. + public Role GetRole(ulong id) { Role result = null; if (_roles?.TryGetValue(id, out result) == true) return result; return null; - } - - /// Creates a new role. - public async Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) + } + public async Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -280,34 +247,30 @@ namespace Discord.Rest return role; } - /// Gets a collection of all users in this guild. - public async Task> GetUsers() - { - var args = new GetGuildMembersParams(); - var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, x)); - } - /// Gets a paged collection of all users in this guild. - public async Task> GetUsers(int limit, int offset) - { - var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; - var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, x)); - } - /// Gets the user in this guild with the provided id, or null if not found. - public async Task GetUser(ulong id) + public virtual async Task GetUser(ulong id) { var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); if (model != null) - return new GuildUser(this, model); + return new GuildUser(this, new User(Discord, model.User), model); return null; } - /// Gets a the current user. - public async Task GetCurrentUser() + public virtual async Task GetCurrentUser() { var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); return await GetUser(currentUser.Id).ConfigureAwait(false); } + public virtual async Task> GetUsers() + { + var args = new GetGuildMembersParams(); + var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); + return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); + } + public virtual async Task> GetUsers(int limit, int offset) + { + var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; + var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); + return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); + } public async Task PruneUsers(int days = 30, bool simulate = false) { var args = new GuildPruneParams() { Days = days }; @@ -324,45 +287,22 @@ namespace Discord.Rest switch (model.Type) { case ChannelType.Text: - default: return new TextChannel(this, model); case ChannelType.Voice: return new VoiceChannel(this, model); + default: + throw new InvalidOperationException($"Unknown channel type: {model.Type}"); } } public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; - IEnumerable IGuild.Emojis => Emojis; - ulong IGuild.EveryoneRoleId => EveryoneRole.Id; - IEnumerable IGuild.Features => Features; + IRole IGuild.EveryoneRole => EveryoneRole; + IReadOnlyCollection IGuild.Emojis => Emojis; + IReadOnlyCollection IGuild.Features => Features; - async Task> IGuild.GetBans() - => await GetBans().ConfigureAwait(false); - async Task IGuild.GetChannel(ulong id) - => await GetChannel(id).ConfigureAwait(false); - async Task> IGuild.GetChannels() - => await GetChannels().ConfigureAwait(false); - async Task IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) - => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); - async Task IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted) - => await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false); - async Task IGuild.CreateTextChannel(string name) - => await CreateTextChannel(name).ConfigureAwait(false); - async Task IGuild.CreateVoiceChannel(string name) - => await CreateVoiceChannel(name).ConfigureAwait(false); - async Task> IGuild.GetInvites() - => await GetInvites().ConfigureAwait(false); - Task IGuild.GetRole(ulong id) - => Task.FromResult(GetRole(id)); - Task> IGuild.GetRoles() - => Task.FromResult>(Roles); - async Task IGuild.GetUser(ulong id) - => await GetUser(id).ConfigureAwait(false); - async Task IGuild.GetCurrentUser() - => await GetCurrentUser().ConfigureAwait(false); - async Task> IGuild.GetUsers() - => await GetUsers().ConfigureAwait(false); + IRole IGuild.GetRole(ulong id) => GetRole(id); } } diff --git a/src/Discord.Net/Entities/Guilds/GuildEmbed.cs b/src/Discord.Net/Entities/Guilds/GuildEmbed.cs new file mode 100644 index 000000000..fdf85abae --- /dev/null +++ b/src/Discord.Net/Entities/Guilds/GuildEmbed.cs @@ -0,0 +1,18 @@ +using Model = Discord.API.GuildEmbed; + +namespace Discord +{ + public struct GuildEmbed + { + public bool IsEnabled { get; private set; } + public ulong? ChannelId { get; private set; } + + public GuildEmbed(bool isEnabled, ulong? channelId) + { + ChannelId = channelId; + IsEnabled = isEnabled; + } + internal GuildEmbed(Model model) + : this(model.Enabled, model.ChannelId) { } + } +} diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs similarity index 68% rename from src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs rename to src/Discord.Net/Entities/Guilds/GuildIntegration.cs index e368cc8d7..52d002f65 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -4,47 +4,37 @@ using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Integration; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class GuildIntegration : IGuildIntegration + internal class GuildIntegration : Entity, IGuildIntegration { - /// - public ulong Id { get; private set; } - /// public string Name { get; private set; } - /// public string Type { get; private set; } - /// public bool IsEnabled { get; private set; } - /// public bool IsSyncing { get; private set; } - /// public ulong ExpireBehavior { get; private set; } - /// public ulong ExpireGracePeriod { get; private set; } - /// public DateTime SyncedAt { get; private set; } - /// public Guild Guild { get; private set; } - /// public Role Role { get; private set; } - /// public User User { get; private set; } - /// public IntegrationAccount Account { get; private set; } - internal DiscordClient Discord => Guild.Discord; - - internal GuildIntegration(Guild guild, Model model) + + public override DiscordClient Discord => Guild.Discord; + + public GuildIntegration(Guild guild, Model model) + : base(model.Id) { Guild = guild; - Update(model); + Update(model, UpdateSource.Creation); } - private void Update(Model model) + private void Update(Model model, UpdateSource source) { - Id = model.Id; + if (source == UpdateSource.Rest && IsAttached) return; + Name = model.Name; Type = model.Type; IsEnabled = model.Enabled; @@ -53,16 +43,14 @@ namespace Discord.Rest ExpireGracePeriod = model.ExpireGracePeriod; SyncedAt = model.SyncedAt; - Role = Guild.GetRole(model.RoleId); - User = new PublicUser(Discord, model.User); + Role = Guild.GetRole(model.RoleId) as Role; + User = new User(Discord, model.User); } - - /// + public async Task Delete() { await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } - /// public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -71,9 +59,8 @@ namespace Discord.Rest func(args); var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } - /// public async Task Sync() { await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); @@ -83,8 +70,7 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; IGuild IGuildIntegration.Guild => Guild; - IRole IGuildIntegration.Role => Role; IUser IGuildIntegration.User => User; - IntegrationAccount IGuildIntegration.Account => Account; + IRole IGuildIntegration.Role => Role; } } diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 2fc618196..013265fd3 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -7,13 +7,17 @@ namespace Discord { public interface IGuild : IDeletable, ISnowflakeEntity, IUpdateable { + /// Gets the name of this guild. + string Name { get; } /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. int AFKTimeout { get; } /// Returns true if this guild is embeddable (e.g. widget) bool IsEmbeddable { get; } - /// Gets the name of this guild. - string Name { get; } int VerificationLevel { get; } + /// Returns the url to this guild's icon, or null if one is not set. + string IconUrl { get; } + /// Returns the url to this guild's splash image, or null if one is not set. + string SplashUrl { get; } /// Gets the id of the AFK voice channel for this guild if set, or null if not. ulong? AFKChannelId { get; } @@ -21,22 +25,19 @@ namespace Discord ulong DefaultChannelId { get; } /// Gets the id of the embed channel for this guild if set, or null if not. ulong? EmbedChannelId { get; } - /// Gets the id of the role containing all users in this guild. - ulong EveryoneRoleId { get; } /// Gets the id of the user that created this guild. ulong OwnerId { get; } - /// Gets the id of the server region hosting this guild's voice channels. + /// Gets the id of the region hosting this guild's voice channels. string VoiceRegionId { get; } - /// Returns the url to this server's icon, or null if one is not set. - string IconUrl { get; } - /// Returns the url to this server's splash image, or null if one is not set. - string SplashUrl { get; } - + /// Gets the built-in role containing all users in this guild. + IRole EveryoneRole { get; } /// Gets a collection of all custom emojis for this guild. - IEnumerable Emojis { get; } + IReadOnlyCollection Emojis { get; } /// Gets a collection of all extra features added to this guild. - IEnumerable Features { get; } + IReadOnlyCollection Features { get; } + /// Gets a collection of all roles in this guild. + IReadOnlyCollection Roles { get; } /// Modifies this guild. Task Modify(Action func); @@ -50,7 +51,7 @@ namespace Discord Task Leave(); /// Gets a collection of all users banned on this guild. - Task> GetBans(); + Task> GetBans(); /// Bans the provided user from this guild and optionally prunes their recent messages. Task AddBan(IUser user, int pruneDays = 0); /// Bans the provided user id from this guild and optionally prunes their recent messages. @@ -61,7 +62,7 @@ namespace Discord Task RemoveBan(ulong userId); /// Gets a collection of all channels in this guild. - Task> GetChannels(); + Task> GetChannels(); /// Gets the channel in this guild with the provided id, or null if not found. Task GetChannel(ulong id); /// Creates a new text channel. @@ -70,7 +71,7 @@ namespace Discord Task CreateVoiceChannel(string name); /// Gets a collection of all invites to this guild. - Task> GetInvites(); + Task> GetInvites(); /// Creates a new invite to this guild. /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. @@ -78,15 +79,13 @@ namespace Discord /// If true, creates a human-readable link. Not supported if maxAge is set to null. Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); - /// Gets a collection of all roles in this guild. - Task> GetRoles(); /// Gets the role in this guild with the provided id, or null if not found. - Task GetRole(ulong id); + IRole GetRole(ulong id); /// Creates a new role. Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); /// Gets a collection of all users in this guild. - Task> GetUsers(); + Task> GetUsers(); /// Gets the user in this guild with the provided id, or null if not found. Task GetUser(ulong id); /// Gets the current user for this guild. diff --git a/src/Discord.Net/Entities/Guilds/IGuildEmbed.cs b/src/Discord.Net/Entities/Guilds/IGuildEmbed.cs deleted file mode 100644 index 0ad23039f..000000000 --- a/src/Discord.Net/Entities/Guilds/IGuildEmbed.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public interface IGuildEmbed : ISnowflakeEntity - { - bool IsEnabled { get; } - ulong? ChannelId { get; } - } -} diff --git a/src/Discord.Net/Entities/Guilds/IUserGuild.cs b/src/Discord.Net/Entities/Guilds/IUserGuild.cs index 962602e4a..b27db9377 100644 --- a/src/Discord.Net/Entities/Guilds/IUserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IUserGuild.cs @@ -4,7 +4,7 @@ { /// Gets the name of this guild. string Name { get; } - /// Returns the url to this server's icon, or null if one is not set. + /// Returns the url to this guild's icon, or null if one is not set. string IconUrl { get; } /// Returns true if the current user owns this guild. bool IsOwner { get; } diff --git a/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs index 22fa6432c..1a76287d8 100644 --- a/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs +++ b/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs @@ -1,7 +1,9 @@ namespace Discord { - public interface IVoiceRegion : IEntity + public interface IVoiceRegion { + /// Gets the unique identifier for this voice region. + string Id { get; } /// Gets the name of this voice region. string Name { get; } /// Returns true if this voice region is exclusive to VIP accounts. diff --git a/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs index db0351bb1..71bcf10ed 100644 --- a/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs +++ b/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs @@ -5,10 +5,7 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct IntegrationAccount { - /// public string Id { get; } - - /// public string Name { get; private set; } public override string ToString() => Name; diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Entities/Guilds/UserGuild.cs similarity index 65% rename from src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs rename to src/Discord.Net/Entities/Guilds/UserGuild.cs index ae5c31da3..4eb45342d 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/UserGuild.cs @@ -1,50 +1,42 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.UserGuild; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class UserGuild : IUserGuild + internal class UserGuild : SnowflakeEntity, IUserGuild { private string _iconId; - - /// - public ulong Id { get; } - internal IDiscordClient Discord { get; } - - /// + public string Name { get; private set; } public bool IsOwner { get; private set; } public GuildPermissions Permissions { get; private set; } - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// + public override DiscordClient Discord { get; } + public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - internal UserGuild(IDiscordClient discord, Model model) + public UserGuild(DiscordClient discord, Model model) + : base(model.Id) { Discord = discord; - Id = model.Id; - - Update(model); + Update(model, UpdateSource.Creation); } - private void Update(Model model) + private void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + _iconId = model.Icon; IsOwner = model.Owner; Name = model.Name; Permissions = new GuildPermissions(model.Permissions); } - /// public async Task Leave() { await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } - /// public async Task Delete() { await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); diff --git a/src/Discord.Net/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Entities/Guilds/VoiceRegion.cs index 126807202..bf61a33a6 100644 --- a/src/Discord.Net/Entities/Guilds/VoiceRegion.cs +++ b/src/Discord.Net/Entities/Guilds/VoiceRegion.cs @@ -4,22 +4,16 @@ using Model = Discord.API.VoiceRegion; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class VoiceRegion : IVoiceRegion + internal class VoiceRegion : IVoiceRegion { - /// public string Id { get; } - /// public string Name { get; } - /// public bool IsVip { get; } - /// public bool IsOptimal { get; } - /// public string SampleHostname { get; } - /// public int SamplePort { get; } - internal VoiceRegion(Model model) + public VoiceRegion(Model model) { Id = model.Id; Name = model.Name; diff --git a/src/Discord.Net/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs index 80f345bda..5d872ca7e 100644 --- a/src/Discord.Net/Entities/IEntity.cs +++ b/src/Discord.Net/Entities/IEntity.cs @@ -4,5 +4,9 @@ namespace Discord { /// Gets the unique identifier for this object. TId Id { get; } + + //TODO: What do we do when an object is destroyed due to reconnect? This summary isn't correct. + /// Returns true if this object is getting live updates from the DiscordClient. + bool IsAttached { get;} } } diff --git a/src/Discord.Net/Entities/IUpdateable.cs b/src/Discord.Net/Entities/IUpdateable.cs index eeb31bf88..4f7d5ed34 100644 --- a/src/Discord.Net/Entities/IUpdateable.cs +++ b/src/Discord.Net/Entities/IUpdateable.cs @@ -4,7 +4,7 @@ namespace Discord { public interface IUpdateable { - /// Ensures this objects's cached properties reflect its current state on the Discord server. + /// Updates this object's properties with its current state. Task Update(); } } diff --git a/src/Discord.Net/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs index 4b0f55f59..eddc3df5a 100644 --- a/src/Discord.Net/Entities/Invites/IInvite.cs +++ b/src/Discord.Net/Entities/Invites/IInvite.cs @@ -18,7 +18,7 @@ namespace Discord /// Gets the id of the guild this invite is linked to. ulong GuildId { get; } - /// Accepts this invite and joins the target server. This will fail on bot accounts. + /// Accepts this invite and joins the target guild. This will fail on bot accounts. Task Accept(); } } diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs index 97e1fc051..c521370ed 100644 --- a/src/Discord.Net/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -5,38 +5,31 @@ using Model = Discord.API.Invite; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Invite : IInvite + internal class Invite : Entity, IInvite { - /// - public string Code { get; } - internal IDiscordClient Discord { get; } + public string ChannelName { get; private set; } + public string GuildName { get; private set; } + public string XkcdCode { get; private set; } - /// - public ulong GuildId { get; private set; } - /// public ulong ChannelId { get; private set; } - /// - public string XkcdCode { get; private set; } - /// - public string GuildName { get; private set; } - /// - public string ChannelName { get; private set; } + public ulong GuildId { get; private set; } + public override DiscordClient Discord { get; } - /// + public string Code => Id; public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; - /// public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; - - internal Invite(IDiscordClient discord, Model model) + public Invite(DiscordClient discord, Model model) + : base(model.Code) { Discord = discord; - Code = model.Code; - Update(model); + Update(model, UpdateSource.Creation); } - protected virtual void Update(Model model) + protected void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + XkcdCode = model.XkcdPass; GuildId = model.Guild.Id; ChannelId = model.Channel.Id; @@ -44,22 +37,16 @@ namespace Discord ChannelName = model.Channel.Name; } - /// public async Task Accept() { await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); } - - /// public async Task Delete() { await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); } - /// public override string ToString() => XkcdUrl ?? Url; private string DebuggerDisplay => $"{XkcdUrl ?? Url} ({GuildName} / {ChannelName})"; - - string IEntity.Id => Code; } } diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index 61f353ebd..a4edc761f 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -2,26 +2,23 @@ namespace Discord { - public class InviteMetadata : Invite, IInviteMetadata + internal class InviteMetadata : Invite, IInviteMetadata { - /// public bool IsRevoked { get; private set; } - /// public bool IsTemporary { get; private set; } - /// public int? MaxAge { get; private set; } - /// public int? MaxUses { get; private set; } - /// public int Uses { get; private set; } - internal InviteMetadata(IDiscordClient client, Model model) + public InviteMetadata(DiscordClient client, Model model) : base(client, model) { - Update(model); + Update(model, UpdateSource.Creation); } - private void Update(Model model) + private void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; diff --git a/src/Discord.Net/Entities/Messages/Embed.cs b/src/Discord.Net/Entities/Messages/Embed.cs index 271e47f66..4e3d065a1 100644 --- a/src/Discord.Net/Entities/Messages/Embed.cs +++ b/src/Discord.Net/Entities/Messages/Embed.cs @@ -2,16 +2,16 @@ namespace Discord { - public struct Embed + internal class Embed : IEmbed { + public string Description { get; } public string Url { get; } - public string Type { get; } public string Title { get; } - public string Description { get; } + public string Type { get; } public EmbedProvider Provider { get; } public EmbedThumbnail Thumbnail { get; } - internal Embed(Model model) + public Embed(Model model) { Url = model.Url; Type = model.Type; diff --git a/src/Discord.Net/Entities/Messages/EmbedProvider.cs b/src/Discord.Net/Entities/Messages/EmbedProvider.cs index 2fce8dfe7..1f1ef6d2d 100644 --- a/src/Discord.Net/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net/Entities/Messages/EmbedProvider.cs @@ -7,10 +7,12 @@ namespace Discord public string Name { get; } public string Url { get; } - internal EmbedProvider(Model model) + public EmbedProvider(string name, string url) { - Name = model.Name; - Url = model.Url; + Name = name; + Url = url; } + internal EmbedProvider(Model model) + : this(model.Name, model.Url) { } } } diff --git a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs index a61323ed6..8630dc473 100644 --- a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs @@ -9,12 +9,15 @@ namespace Discord public int? Height { get; } public int? Width { get; } - internal EmbedThumbnail(Model model) + public EmbedThumbnail(string url, string proxyUrl, int? height, int? width) { - Url = model.Url; - ProxyUrl = model.ProxyUrl; - Height = model.Height; - Width = model.Width; + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; } + + internal EmbedThumbnail(Model model) + : this(model.Url, model.ProxyUrl, model.Height, model.Width) { } } } diff --git a/src/Discord.Net/Entities/Messages/IEmbed.cs b/src/Discord.Net/Entities/Messages/IEmbed.cs new file mode 100644 index 000000000..e0080f320 --- /dev/null +++ b/src/Discord.Net/Entities/Messages/IEmbed.cs @@ -0,0 +1,12 @@ +namespace Discord +{ + public interface IEmbed + { + string Url { get; } + string Type { get; } + string Title { get; } + string Description { get; } + EmbedProvider Provider { get; } + EmbedThumbnail Thumbnail { get; } + } +} diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 35107ccf2..d9cd9ab04 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Discord { - public interface IMessage : IDeletable, ISnowflakeEntity + public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable { /// Gets the time of this message's last edit, if any. DateTime? EditedTimestamp { get; } @@ -16,23 +16,22 @@ namespace Discord /// Returns the text for this message after mention processing. string Text { get; } /// Gets the time this message was sent. - DateTime Timestamp { get; } //TODO: Is this different from IHasSnowflake.CreatedAt? + DateTime Timestamp { get; } /// Gets the channel this message was sent to. IMessageChannel Channel { get; } /// Gets the author of this message. IUser Author { get; } - /// Returns a collection of all attachments included in this message. - IReadOnlyList Attachments { get; } + IReadOnlyCollection Attachments { get; } /// Returns a collection of all embeds included in this message. - IReadOnlyList Embeds { get; } + IReadOnlyCollection Embeds { get; } /// Returns a collection of channel ids mentioned in this message. - IReadOnlyList MentionedChannelIds { get; } + IReadOnlyCollection MentionedChannelIds { get; } /// Returns a collection of role ids mentioned in this message. - IReadOnlyList MentionedRoleIds { get; } + IReadOnlyCollection MentionedRoleIds { get; } /// Returns a collection of user ids mentioned in this message. - IReadOnlyList MentionedUsers { get; } + IReadOnlyCollection MentionedUsers { get; } /// Modifies this message. Task Modify(Action func); diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs similarity index 63% rename from src/Discord.Net/Rest/Entities/Message.cs rename to src/Discord.Net/Entities/Messages/Message.cs index 319394214..e551616d2 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -6,55 +6,40 @@ using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Message; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Message : IMessage - { - /// - public ulong Id { get; } - - /// + internal class Message : SnowflakeEntity, IMessage + { public DateTime? EditedTimestamp { get; private set; } - /// public bool IsTTS { get; private set; } - /// public string RawText { get; private set; } - /// public string Text { get; private set; } - /// public DateTime Timestamp { get; private set; } - - /// + public IMessageChannel Channel { get; } - /// public IUser Author { get; } + + public ImmutableArray Attachments { get; private set; } + public ImmutableArray Embeds { get; private set; } + public ImmutableArray MentionedChannelIds { get; private set; } + public ImmutableArray MentionedRoleIds { get; private set; } + public ImmutableArray MentionedUsers { get; private set; } + + public override DiscordClient Discord => (Channel as Entity).Discord; - /// - public IReadOnlyList Attachments { get; private set; } - /// - public IReadOnlyList Embeds { get; private set; } - /// - public IReadOnlyList MentionedUsers { get; private set; } - /// - public IReadOnlyList MentionedChannelIds { get; private set; } - /// - public IReadOnlyList MentionedRoleIds { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; - - internal Message(IMessageChannel channel, Model model) + public Message(IMessageChannel channel, IUser author, Model model) + : base(model.Id) { - Id = model.Id; Channel = channel; - Author = new PublicUser(Discord, model.Author); + Author = author; - Update(model); + Update(model, UpdateSource.Creation); } - private void Update(Model model) + private void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + var guildChannel = Channel as GuildChannel; var guild = guildChannel?.Guild; var discord = Discord; @@ -72,7 +57,7 @@ namespace Discord.Rest Attachments = ImmutableArray.Create(attachments); } else - Attachments = Array.Empty(); + Attachments = ImmutableArray.Create(); if (model.Embeds.Length > 0) { @@ -82,17 +67,17 @@ namespace Discord.Rest Embeds = ImmutableArray.Create(embeds); } else - Embeds = Array.Empty(); + Embeds = ImmutableArray.Create(); if (guildChannel != null && model.Mentions.Length > 0) { - var mentions = new PublicUser[model.Mentions.Length]; + var mentions = new User[model.Mentions.Length]; for (int i = 0; i < model.Mentions.Length; i++) - mentions[i] = new PublicUser(discord, model.Mentions[i]); + mentions[i] = new User(discord, model.Mentions[i]); MentionedUsers = ImmutableArray.Create(mentions); } else - MentionedUsers = Array.Empty(); + MentionedUsers = ImmutableArray.Create(); if (guildChannel != null) { @@ -105,14 +90,20 @@ namespace Discord.Rest } else { - MentionedChannelIds = Array.Empty(); - MentionedRoleIds = Array.Empty(); + MentionedChannelIds = ImmutableArray.Create(); + MentionedRoleIds = ImmutableArray.Create(); } Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); } - /// + public async Task Update() + { + if (IsAttached) throw new NotSupportedException(); + + var model = await Discord.ApiClient.GetChannelMessage(Channel.Id, Id).ConfigureAwait(false); + Update(model, UpdateSource.Rest); + } public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -126,10 +117,8 @@ namespace Discord.Rest model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); - Update(model); - } - - /// + Update(model, UpdateSource.Rest); + } public async Task Delete() { var guildChannel = Channel as GuildChannel; @@ -140,9 +129,12 @@ namespace Discord.Rest } public override string ToString() => Text; - private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; + private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Length > 0 ? $" [{Attachments.Length} Attachments]" : "")}"; - IUser IMessage.Author => Author; - IReadOnlyList IMessage.MentionedUsers => MentionedUsers; + IReadOnlyCollection IMessage.Attachments => Attachments; + IReadOnlyCollection IMessage.Embeds => Embeds; + IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannelIds; + IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoleIds; + IReadOnlyCollection IMessage.MentionedUsers => MentionedUsers; } } diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs index f5760f1a9..5084d6ac6 100644 --- a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -144,7 +144,7 @@ namespace Discord } return perms; } - /// + public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; } diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs index 899cac80a..4240a6cc3 100644 --- a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -144,7 +144,7 @@ namespace Discord } return perms; } - /// + public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; } diff --git a/src/Discord.Net/Entities/Permissions/Overwrite.cs b/src/Discord.Net/Entities/Permissions/Overwrite.cs index d964e7068..7333d93e1 100644 --- a/src/Discord.Net/Entities/Permissions/Overwrite.cs +++ b/src/Discord.Net/Entities/Permissions/Overwrite.cs @@ -12,11 +12,14 @@ namespace Discord public OverwritePermissions Permissions { get; } /// Creates a new Overwrite with provided target information and modified permissions. - internal Overwrite(Model model) + public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions) { - TargetId = model.TargetId; - TargetType = model.TargetType; - Permissions = new OverwritePermissions(model.Allow, model.Deny); + TargetId = targetId; + TargetType = targetType; + Permissions = permissions; } + + internal Overwrite(Model model) + : this(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)) { } } } diff --git a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs index 7c448522e..ee425a755 100644 --- a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs @@ -135,7 +135,7 @@ namespace Discord } return perms; } - /// + public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; private string DebuggerDisplay => $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs index 3cd17e66e..4c920dd07 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -90,8 +90,8 @@ namespace Discord { var roles = user.Roles; ulong newPermissions = 0; - for (int i = 0; i < roles.Count; i++) - newPermissions |= roles[i].Permissions.RawValue; + foreach (var role in roles) + newPermissions |= role.Permissions.RawValue; return newPermissions; } @@ -110,25 +110,26 @@ namespace Discord { //Start with this user's guild permissions resolvedPermissions = guildPermissions; - var overwrites = channel.PermissionOverwrites; - Overwrite entry; + OverwritePermissions? perms; var roles = user.Roles; if (roles.Count > 0) { - for (int i = 0; i < roles.Count; i++) + ulong deniedPermissions = 0UL, allowedPermissions = 0UL; + foreach (var role in roles) { - if (overwrites.TryGetValue(roles[i].Id, out entry)) - resolvedPermissions &= ~entry.Permissions.DenyValue; - } - for (int i = 0; i < roles.Count; i++) - { - if (overwrites.TryGetValue(roles[i].Id, out entry)) - resolvedPermissions |= entry.Permissions.AllowValue; + perms = channel.GetPermissionOverwrite(role); + if (perms != null) + { + deniedPermissions |= perms.Value.DenyValue; + allowedPermissions |= perms.Value.AllowValue; + } } + resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; } - if (overwrites.TryGetValue(user.Id, out entry)) - resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; + perms = channel.GetPermissionOverwrite(user); + if (perms != null) + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; #if CSHARP7 switch (channel) diff --git a/src/Discord.Net/Entities/Roles/IRole.cs b/src/Discord.Net/Entities/Roles/IRole.cs index e51769790..36d0ce641 100644 --- a/src/Discord.Net/Entities/Roles/IRole.cs +++ b/src/Discord.Net/Entities/Roles/IRole.cs @@ -11,7 +11,7 @@ namespace Discord Color Color { get; } /// Returns true if users of this role are separated in the user list. bool IsHoisted { get; } - /// Returns true if this role is automatically managed by the Discord server. + /// Returns true if this role is automatically managed by Discord. bool IsManaged { get; } /// Gets the name of this role. string Name { get; } @@ -25,8 +25,5 @@ namespace Discord /// Modifies this role. Task Modify(Action func); - - /// Returns a collection of all users that have been assigned this role. - Task> GetUsers(); } } \ No newline at end of file diff --git a/src/Discord.Net/Rest/Entities/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs similarity index 54% rename from src/Discord.Net/Rest/Entities/Role.cs rename to src/Discord.Net/Entities/Roles/Role.cs index 20ed0940e..578930b42 100644 --- a/src/Discord.Net/Rest/Entities/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -1,51 +1,41 @@ using Discord.API.Rest; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Role; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Role : IRole, IMentionable + internal class Role : SnowflakeEntity, IRole, IMentionable { - /// - public ulong Id { get; } - /// Returns the guild this role belongs to. public Guild Guild { get; } - - /// + public Color Color { get; private set; } - /// public bool IsHoisted { get; private set; } - /// public bool IsManaged { get; private set; } - /// public string Name { get; private set; } - /// public GuildPermissions Permissions { get; private set; } - /// public int Position { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// + public bool IsEveryone => Id == Guild.Id; - /// public string Mention => MentionUtils.Mention(this); - internal DiscordClient Discord => Guild.Discord; + public override DiscordClient Discord => Guild.Discord; - internal Role(Guild guild, Model model) + public Role(Guild guild, Model model) + : base(model.Id) { - Id = model.Id; Guild = guild; - Update(model); + Update(model, UpdateSource.Creation); } - internal void Update(Model model) + public void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + Name = model.Name; IsHoisted = model.Hoist.Value; IsManaged = model.Managed.Value; @@ -53,7 +43,7 @@ namespace Discord.Rest Color = new Color(model.Color.Value); Permissions = new GuildPermissions(model.Permissions.Value); } - /// Modifies the properties of this role. + public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -61,23 +51,16 @@ namespace Discord.Rest var args = new ModifyGuildRoleParams(); func(args); var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); - Update(response); + Update(response, UpdateSource.Rest); } - /// Deletes this message. public async Task Delete() - => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); - - /// + { + await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); + } + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; ulong IRole.GuildId => Guild.Id; - - async Task> IRole.GetUsers() - { - //TODO: Rethink this, it isn't paginated or anything... - var models = await Discord.ApiClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); - return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x)); - } } } diff --git a/src/Discord.Net/Entities/SnowflakeEntity.cs b/src/Discord.Net/Entities/SnowflakeEntity.cs new file mode 100644 index 000000000..5b67e6e80 --- /dev/null +++ b/src/Discord.Net/Entities/SnowflakeEntity.cs @@ -0,0 +1,15 @@ +using System; + +namespace Discord +{ + internal abstract class SnowflakeEntity : Entity, ISnowflakeEntity + { + //TODO: Candidate for Extension Property. Lets us remove this class. + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + + public SnowflakeEntity(ulong id) + : base(id) + { + } + } +} diff --git a/src/Discord.Net/Entities/UpdateSource.cs b/src/Discord.Net/Entities/UpdateSource.cs new file mode 100644 index 000000000..6c56416e7 --- /dev/null +++ b/src/Discord.Net/Entities/UpdateSource.cs @@ -0,0 +1,9 @@ +namespace Discord +{ + internal enum UpdateSource + { + Creation, + Rest, + WebSocket + } +} diff --git a/src/Discord.Net/Entities/Users/Connection.cs b/src/Discord.Net/Entities/Users/Connection.cs index 10852820e..17507ee44 100644 --- a/src/Discord.Net/Entities/Users/Connection.cs +++ b/src/Discord.Net/Entities/Users/Connection.cs @@ -5,19 +5,18 @@ using Model = Discord.API.Connection; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Connection : IConnection + internal class Connection : IConnection { public string Id { get; } public string Type { get; } public string Name { get; } public bool IsRevoked { get; } - public IEnumerable IntegrationIds { get; } + public IReadOnlyCollection IntegrationIds { get; } public Connection(Model model) { Id = model.Id; - Type = model.Type; Name = model.Name; IsRevoked = model.Revoked; diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs index ee5559165..18cc4665a 100644 --- a/src/Discord.Net/Entities/Users/Game.cs +++ b/src/Discord.Net/Entities/Users/Game.cs @@ -1,4 +1,6 @@ -namespace Discord +using Model = Discord.API.Game; + +namespace Discord { public struct Game { @@ -6,17 +8,15 @@ public string StreamUrl { get; } public StreamType StreamType { get; } - public Game(string name) - { - Name = name; - StreamUrl = null; - StreamType = StreamType.NotStreaming; - } public Game(string name, string streamUrl, StreamType type) { Name = name; StreamUrl = streamUrl; StreamType = type; } + public Game(string name) + : this(name, null, StreamType.NotStreaming) { } + internal Game(Model model) + : this(model.Name, model.StreamUrl, model.StreamType ?? StreamType.NotStreaming) { } } } diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs similarity index 63% rename from src/Discord.Net/Rest/Entities/Users/GuildUser.cs rename to src/Discord.Net/Entities/Users/GuildUser.cs index 33d100255..ec5c5784f 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -6,70 +6,64 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; -namespace Discord.Rest +namespace Discord { - public class GuildUser : User, IGuildUser + internal class GuildUser : IGuildUser, ISnowflakeEntity { - private ImmutableArray _roles; - - public Guild Guild { get; } - - /// public bool IsDeaf { get; private set; } - /// public bool IsMute { get; private set; } - /// public DateTime JoinedAt { get; private set; } - /// public string Nickname { get; private set; } - - /// public GuildPermissions GuildPermissions { get; private set; } - /// - public IReadOnlyList Roles => _roles; - internal override DiscordClient Discord => Guild.Discord; + public Guild Guild { get; private set; } + public User User { get; private set; } + public ImmutableArray Roles { get; private set; } + + public ulong Id => User.Id; + public string AvatarUrl => User.AvatarUrl; + public DateTime CreatedAt => User.CreatedAt; + public ushort Discriminator => User.Discriminator; + public Game? Game => User.Game; + public bool IsAttached => User.IsAttached; + public bool IsBot => User.IsBot; + public string Mention => User.Mention; + public UserStatus Status => User.Status; + public string Username => User.Username; - internal GuildUser(Guild guild, Model model) - : base(model.User) + public DiscordClient Discord => Guild.Discord; + + public GuildUser(Guild guild, User user, Model model) { Guild = guild; - Update(model); + Update(model, UpdateSource.Creation); } - internal void Update(Model model) + private void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + IsDeaf = model.Deaf; IsMute = model.Mute; JoinedAt = model.JoinedAt.Value; Nickname = model.Nick; var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles.Add(Guild.EveryoneRole); + roles.Add(Guild.EveryoneRole as Role); for (int i = 0; i < model.Roles.Length; i++) - roles.Add(Guild.GetRole(model.Roles[i])); - _roles = roles.ToImmutable(); + roles.Add(Guild.GetRole(model.Roles[i]) as Role); + Roles = roles.ToImmutable(); GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } public async Task Update() { - var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); - Update(model); - } + if (IsAttached) throw new NotSupportedException(); - public async Task Kick() - { - await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); - } - - public ChannelPermissions GetPermissions(IGuildChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); + var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); + Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -82,7 +76,7 @@ namespace Discord.Rest { var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" }; await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); - args.Nickname = new API.Optional(); //Remove + args.Nickname = new Optional(); //Remove } if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) @@ -95,18 +89,24 @@ namespace Discord.Rest if (args.Nickname.IsSpecified) Nickname = args.Nickname.Value ?? ""; if (args.Roles.IsSpecified) - _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); + Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray(); } } + public async Task Kick() + { + await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); + } + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); + } + + public Task CreateDMChannel() => User.CreateDMChannel(); IGuild IGuildUser.Guild => Guild; - IReadOnlyList IGuildUser.Roles => Roles; + IReadOnlyCollection IGuildUser.Roles => Roles; IVoiceChannel IGuildUser.VoiceChannel => null; - - GuildPermissions IGuildUser.GetGuildPermissions() - => GuildPermissions; - ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) - => GetPermissions(channel); } } diff --git a/src/Discord.Net/Entities/Users/IConnection.cs b/src/Discord.Net/Entities/Users/IConnection.cs index 6540c147e..cc981ccf0 100644 --- a/src/Discord.Net/Entities/Users/IConnection.cs +++ b/src/Discord.Net/Entities/Users/IConnection.cs @@ -9,6 +9,6 @@ namespace Discord string Name { get; } bool IsRevoked { get; } - IEnumerable IntegrationIds { get; } + IReadOnlyCollection IntegrationIds { get; } } } diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index f5d17688c..63b7c12fb 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -16,16 +16,16 @@ namespace Discord DateTime JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } + /// Gets the guild-level permissions granted to this user by their roles. + GuildPermissions GuildPermissions { get; } /// Gets the guild for this guild-user pair. IGuild Guild { get; } /// Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. - IReadOnlyList Roles { get; } + IReadOnlyCollection Roles { get; } /// Gets the voice channel this user is currently in, if any. IVoiceChannel VoiceChannel { get; } - /// Gets the guild-level permissions granted to this user by their roles. - GuildPermissions GetGuildPermissions(); /// Gets the channel-level permissions granted to this user for a given channel. ChannelPermissions GetPermissions(IGuildChannel channel); @@ -34,4 +34,4 @@ namespace Discord /// Modifies this user's properties in this guild. Task Modify(Action func); } -} \ No newline at end of file +} diff --git a/src/Discord.Net/Entities/Users/IPresence.cs b/src/Discord.Net/Entities/Users/IPresence.cs new file mode 100644 index 000000000..7f182241b --- /dev/null +++ b/src/Discord.Net/Entities/Users/IPresence.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public interface IPresence + { + /// Gets the game this user is currently playing, if any. + Game? Game { get; } + /// Gets the current status of this user. + UserStatus Status { get; } + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index c4754f3e3..8de684b7a 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -2,18 +2,14 @@ using System.Threading.Tasks; namespace Discord { - public interface IUser : ISnowflakeEntity, IMentionable + public interface IUser : ISnowflakeEntity, IMentionable, IPresence { /// Gets the url to this user's avatar. string AvatarUrl { get; } - /// Gets the game this user is currently playing, if any. - Game? CurrentGame { get; } /// Gets the per-username unique id for this user. ushort Discriminator { get; } /// Returns true if this user is a bot account. bool IsBot { get; } - /// Gets the current status of this user. - UserStatus Status { get; } /// Gets the username for this user. string Username { get; } diff --git a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs similarity index 60% rename from src/Discord.Net/Rest/Entities/Users/SelfUser.cs rename to src/Discord.Net/Entities/Users/SelfUser.cs index a821b369b..d650c29bf 100644 --- a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -3,38 +3,34 @@ using System; using System.Threading.Tasks; using Model = Discord.API.User; -namespace Discord.Rest +namespace Discord { - public class SelfUser : User, ISelfUser - { - internal override DiscordClient Discord { get; } - - /// + internal class SelfUser : User, ISelfUser + { public string Email { get; private set; } - /// public bool IsVerified { get; private set; } - internal SelfUser(DiscordClient discord, Model model) - : base(model) + public SelfUser(DiscordClient discord, Model model) + : base(discord, model) { - Discord = discord; } - internal override void Update(Model model) + public override void Update(Model model, UpdateSource source) { - base.Update(model); + if (source == UpdateSource.Rest && IsAttached) return; + + base.Update(model, source); Email = model.Email; IsVerified = model.IsVerified; } - - /// + public async Task Update() { - var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); - Update(model); - } + if (IsAttached) throw new NotSupportedException(); - /// + var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); + Update(model, UpdateSource.Rest); + } public async Task Modify(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); @@ -42,7 +38,7 @@ namespace Discord.Rest var args = new ModifyCurrentUserParams(); func(args); var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); - Update(model); + Update(model, UpdateSource.Rest); } } } diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs similarity index 52% rename from src/Discord.Net/Rest/Entities/Users/User.cs rename to src/Discord.Net/Entities/Users/User.cs index 26754fc18..5ce282ddf 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -1,68 +1,52 @@ using Discord.API.Rest; -using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; -namespace Discord.Rest +namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] - public abstract class User : IUser + internal class User : SnowflakeEntity, IUser { private string _avatarId; - - /// - public ulong Id { get; } - internal abstract DiscordClient Discord { get; } - - /// + public ushort Discriminator { get; private set; } - /// public bool IsBot { get; private set; } - /// public string Username { get; private set; } - /// + public override DiscordClient Discord { get; } + public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// + public virtual Game? Game => null; public string Mention => MentionUtils.Mention(this, false); - /// public string NicknameMention => MentionUtils.Mention(this, true); + public virtual UserStatus Status => UserStatus.Unknown; - internal User(Model model) + public User(DiscordClient discord, Model model) + : base(model.Id) { - Id = model.Id; - - Update(model); + Discord = discord; + Update(model, UpdateSource.Creation); } - internal virtual void Update(Model model) + public virtual void Update(Model model, UpdateSource source) { + if (source == UpdateSource.Rest && IsAttached) return; + _avatarId = model.Avatar; Discriminator = model.Discriminator; IsBot = model.Bot; Username = model.Username; } - protected virtual async Task CreateDMChannelInternal() + public async Task CreateDMChannel() { var args = new CreateDMChannelParams { RecipientId = Id }; var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); - return new DMChannel(Discord, model); + return new DMChannel(Discord, this, model); } public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - - /// - Game? IUser.CurrentGame => null; - /// - UserStatus IUser.Status => UserStatus.Unknown; - - /// - async Task IUser.CreateDMChannel() - => await CreateDMChannelInternal().ConfigureAwait(false); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs new file mode 100644 index 000000000..33411ee17 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using MessageModel = Discord.API.Message; +using Model = Discord.API.Channel; + +namespace Discord +{ + internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel + { + private readonly MessageCache _messages; + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; + public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); + + public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) + : base(discord, recipient, model) + { + _messages = new MessageCache(Discord, this); + } + + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult(Members); + public override Task> GetUsers(int limit, int offset) + => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); + public IUser GetCachedUser(ulong id) + { + var currentUser = Discord.CurrentUser; + if (id == Recipient.Id) + return Recipient; + else if (id == currentUser.Id) + return currentUser; + else + return null; + } + + public override async Task GetMessage(ulong id) + { + return await _messages.Download(id).ConfigureAwait(false); + } + public override async Task> GetMessages(int limit) + { + return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); + } + public override async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + { + return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); + } + public CachedMessage AddCachedMessage(IUser author, MessageModel model) + { + var msg = new CachedMessage(this, author, model); + _messages.Add(msg); + return msg; + } + public CachedMessage GetCachedMessage(ulong id) + { + return _messages.Get(id); + } + public CachedMessage RemoveCachedMessage(ulong id) + { + return _messages.Remove(id); + } + + public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; + + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs new file mode 100644 index 000000000..2c5f02f33 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -0,0 +1,171 @@ +using Discord.Data; +using Discord.Extensions; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using ChannelModel = Discord.API.Channel; +using ExtendedModel = Discord.API.Gateway.ExtendedGuild; +using MemberModel = Discord.API.GuildMember; +using Model = Discord.API.Guild; +using PresenceModel = Discord.API.Presence; + +namespace Discord +{ + internal class CachedGuild : Guild, ICachedEntity + { + private ConcurrentHashSet _channels; + private ConcurrentDictionary _members; + private ConcurrentDictionary _presences; + private int _userCount; + + public bool Available { get; private set; } //TODO: Add to IGuild + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); + public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); + public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); + + public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) + { + } + + public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) + { + if (source == UpdateSource.Rest && IsAttached) return; + + Available = !(model.Unavailable ?? false); + if (!Available) + { + if (_channels == null) + _channels = new ConcurrentHashSet(); + if (_members == null) + _members = new ConcurrentDictionary(); + if (_presences == null) + _presences = new ConcurrentDictionary(); + if (_roles == null) + _roles = new ConcurrentDictionary(); + if (Emojis == null) + Emojis = ImmutableArray.Create(); + if (Features == null) + Features = ImmutableArray.Create(); + return; + } + + base.Update(model as Model, source); + + _userCount = model.MemberCount; + + var channels = new ConcurrentHashSet(); + if (model.Channels != null) + { + for (int i = 0; i < model.Channels.Length; i++) + AddCachedChannel(model.Channels[i], channels, dataStore); + } + _channels = channels; + + var presences = new ConcurrentDictionary(); + if (model.Presences != null) + { + for (int i = 0; i < model.Presences.Length; i++) + AddCachedPresence(model.Presences[i], presences); + } + _presences = presences; + + var members = new ConcurrentDictionary(); + if (model.Members != null) + { + for (int i = 0; i < model.Members.Length; i++) + AddCachedUser(model.Members[i], members, dataStore); + } + _members = members; + } + + public override Task GetChannel(ulong id) => Task.FromResult(GetCachedChannel(id)); + public override Task> GetChannels() => Task.FromResult>(Channels); + public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet channels = null, DataStore dataStore = null) + { + var channel = ToChannel(model); + (dataStore ?? Discord.DataStore).AddChannel(channel); + (channels ?? _channels).TryAdd(model.Id); + return channel; + } + public ICachedGuildChannel GetCachedChannel(ulong id) + { + return Discord.DataStore.GetChannel(id) as ICachedGuildChannel; + } + public ICachedGuildChannel RemoveCachedChannel(ulong id, ConcurrentHashSet channels = null, DataStore dataStore = null) + { + (channels ?? _channels).TryRemove(id); + return (dataStore ?? Discord.DataStore).RemoveChannel(id) as ICachedGuildChannel; + } + + public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) + { + var game = model.Game != null ? new Game(model.Game) : (Game?)null; + var presence = new Presence(model.Status, game); + (presences ?? _presences)[model.User.Id] = presence; + return presence; + } + public Presence? GetCachedPresence(ulong id) + { + Presence presence; + if (_presences.TryGetValue(id, out presence)) + return presence; + return null; + } + public Presence? RemoveCachedPresence(ulong id) + { + Presence presence; + if (_presences.TryRemove(id, out presence)) + return presence; + return null; + } + + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task GetCurrentUser() + => Task.FromResult(CurrentUser); + public override Task> GetUsers() + => Task.FromResult>(Members); + //TODO: Is there a better way of exposing pagination? + public override Task> GetUsers(int limit, int offset) + => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); + public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary members = null, DataStore dataStore = null) + { + var user = Discord.AddCachedUser(model.User); + var member = new CachedGuildUser(this, user, model); + (members ?? _members)[user.Id] = member; + user.AddRef(); + return member; + } + public CachedGuildUser GetCachedUser(ulong id) + { + CachedGuildUser member; + if (_members.TryGetValue(id, out member)) + return member; + return null; + } + public CachedGuildUser RemoveCachedUser(ulong id) + { + CachedGuildUser member; + if (_members.TryRemove(id, out member)) + return member; + return null; + } + + new internal ICachedGuildChannel ToChannel(ChannelModel model) + { + switch (model.Type) + { + case ChannelType.Text: + return new CachedTextChannel(this, model); + case ChannelType.Voice: + return new CachedVoiceChannel(this, model); + default: + throw new InvalidOperationException($"Unknown channel type: {model.Type}"); + } + } + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs new file mode 100644 index 000000000..f1bfa9d14 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -0,0 +1,16 @@ +using Model = Discord.API.GuildMember; + +namespace Discord +{ + internal class CachedGuildUser : GuildUser, ICachedEntity + { + public VoiceChannel VoiceChannel { get; private set; } + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + + public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) + : base(guild, user, model) + { + } + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedMessage.cs b/src/Discord.Net/Entities/WebSocket/CachedMessage.cs new file mode 100644 index 000000000..72edb107d --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedMessage.cs @@ -0,0 +1,17 @@ +using Model = Discord.API.Message; + +namespace Discord +{ + internal class CachedMessage : Message, ICachedEntity + { + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new ICachedMessageChannel Channel => base.Channel as ICachedMessageChannel; + + public CachedMessage(ICachedMessageChannel channel, IUser author, Model model) + : base(channel, author, model) + { + } + + public CachedMessage Clone() => MemberwiseClone() as CachedMessage; + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs new file mode 100644 index 000000000..142cf15f9 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -0,0 +1,58 @@ +using ChannelModel = Discord.API.Channel; +using Model = Discord.API.User; + +namespace Discord +{ + internal class CachedPublicUser : User, ICachedEntity + { + private int _references; + + public CachedDMChannel DMChannel { get; private set; } + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + + public CachedPublicUser(DiscordSocketClient discord, Model model) + : base(discord, model) + { + } + + public CachedDMChannel SetDMChannel(ChannelModel model) + { + lock (this) + { + var channel = new CachedDMChannel(Discord, this, model); + DMChannel = channel; + return channel; + } + } + public CachedDMChannel RemoveDMChannel(ulong id) + { + lock (this) + { + var channel = DMChannel; + if (channel.Id == id) + { + DMChannel = null; + return channel; + } + return null; + } + } + + public void AddRef() + { + lock (this) + _references++; + } + public void RemoveRef() + { + lock (this) + { + if (--_references == 0 && DMChannel == null) + Discord.RemoveCachedUser(Id); + } + } + + public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs new file mode 100644 index 000000000..fe4a264c8 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs @@ -0,0 +1,16 @@ +using Model = Discord.API.User; + +namespace Discord +{ + internal class CachedSelfUser : SelfUser, ICachedEntity + { + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + + public CachedSelfUser(DiscordSocketClient discord, Model model) + : base(discord, model) + { + } + + public CachedSelfUser Clone() => MemberwiseClone() as CachedSelfUser; + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs new file mode 100644 index 000000000..be3d59677 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using MessageModel = Discord.API.Message; +using Model = Discord.API.Channel; + +namespace Discord +{ + internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel + { + private readonly MessageCache _messages; + + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new CachedGuild Guild => base.Guild as CachedGuild; + + public IReadOnlyCollection Members + => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); + + public CachedTextChannel(CachedGuild guild, Model model) + : base(guild, model) + { + _messages = new MessageCache(Discord, this); + } + + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult(Members); + public override Task> GetUsers(int limit, int offset) + => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); + public IGuildUser GetCachedUser(ulong id) + { + var user = Guild.GetCachedUser(id); + if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) + return user; + return null; + } + + public override async Task GetMessage(ulong id) + { + return await _messages.Download(id).ConfigureAwait(false); + } + public override async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + { + return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); + } + public override async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + { + return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); + } + + public CachedMessage AddCachedMessage(IUser author, MessageModel model) + { + var msg = new CachedMessage(this, author, model); + _messages.Add(msg); + return msg; + } + public CachedMessage GetCachedMessage(ulong id) + { + return _messages.Get(id); + } + public CachedMessage RemoveCachedMessage(ulong id) + { + return _messages.Remove(id); + } + + public CachedTextChannel Clone() => MemberwiseClone() as CachedTextChannel; + + IReadOnlyCollection ICachedMessageChannel.Members => Members; + + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs new file mode 100644 index 000000000..6d090d9fa --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + internal class CachedVoiceChannel : VoiceChannel, ICachedGuildChannel + { + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new CachedGuild Guild => base.Guild as CachedGuild; + + public IReadOnlyCollection Members + => Guild.Members.Where(x => x.VoiceChannel.Id == Id).ToImmutableArray(); + + public CachedVoiceChannel(CachedGuild guild, Model model) + : base(guild, model) + { + } + + public override Task GetUser(ulong id) + => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() + => Task.FromResult(Members); + public override Task> GetUsers(int limit, int offset) + => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); + public IGuildUser GetCachedUser(ulong id) + { + var user = Guild.GetCachedUser(id); + if (user != null && user.VoiceChannel.Id == Id) + return user; + return null; + } + + public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; + } +} diff --git a/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs new file mode 100644 index 000000000..933ce6226 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs @@ -0,0 +1,6 @@ +namespace Discord +{ + internal interface ICachedChannel : IChannel, ICachedEntity + { + } +} diff --git a/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs b/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs new file mode 100644 index 000000000..fa004844b --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + interface ICachedEntity : IEntity + { + DiscordSocketClient Discord { get; } + } +} diff --git a/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs new file mode 100644 index 000000000..dc13d1eab --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs @@ -0,0 +1,6 @@ +namespace Discord +{ + internal interface ICachedGuildChannel : ICachedChannel, IGuildChannel + { + } +} diff --git a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs new file mode 100644 index 000000000..5db4a28a7 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using MessageModel = Discord.API.Message; + +namespace Discord +{ + internal interface ICachedMessageChannel : ICachedChannel, IMessageChannel + { + IReadOnlyCollection Members { get; } + + CachedMessage AddCachedMessage(IUser author, MessageModel model); + new CachedMessage GetCachedMessage(ulong id); + CachedMessage RemoveCachedMessage(ulong id); + + IUser GetCachedUser(ulong id); + } +} diff --git a/src/Discord.Net/Entities/Users/IVoiceState.cs.old b/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old similarity index 90% rename from src/Discord.Net/Entities/Users/IVoiceState.cs.old rename to src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old index 5044126a9..0937f5049 100644 --- a/src/Discord.Net/Entities/Users/IVoiceState.cs.old +++ b/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old @@ -3,7 +3,7 @@ using Model = Discord.API.MemberVoiceState; namespace Discord.WebSocket { - public class VoiceState + internal class VoiceState : IVoiceState { [Flags] private enum VoiceStates : byte @@ -22,7 +22,7 @@ namespace Discord.WebSocket public ulong UserId { get; } /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel { get; internal set; } + public VoiceChannel VoiceChannel { get; set; } /// Returns true if this user has marked themselves as muted. public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; @@ -35,13 +35,13 @@ namespace Discord.WebSocket /// Returns true if the guild is temporarily blocking audio to/from this user. public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; - internal VoiceState(ulong userId, Guild guild) + public VoiceState(ulong userId, Guild guild) { UserId = userId; Guild = guild; } - internal void Update(Model model) + private void Update(Model model, UpdateSource source) { if (model.IsMuted == true) _voiceStates |= VoiceStates.Muted; diff --git a/src/Discord.Net/Entities/WebSocket/Presence.cs b/src/Discord.Net/Entities/WebSocket/Presence.cs new file mode 100644 index 000000000..349751351 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/Presence.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + internal struct Presence : IPresence + { + public UserStatus Status { get; } + public Game? Game { get; } + + public Presence(UserStatus status, Game? game) + { + Status = status; + Game = game; + } + } +} diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs new file mode 100644 index 000000000..65785f643 --- /dev/null +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Discord.Extensions +{ + internal static class CollectionExtensions + { + public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) + => new ConcurrentDictionaryWrapper>(source, source.Select(x => x.Value)); + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) + => new ConcurrentDictionaryWrapper(source, query); + } + + internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection + { + private readonly IReadOnlyCollection _source; + private readonly IEnumerable _query; + + public int Count => _source.Count; + + public ConcurrentDictionaryWrapper(IReadOnlyCollection source, IEnumerable query) + { + _source = source; + _query = query; + } + + public IEnumerator GetEnumerator() => _query.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _query.GetEnumerator(); + } +} diff --git a/src/Discord.Net/Extensions/DiscordClientExtensions.cs b/src/Discord.Net/Extensions/DiscordClientExtensions.cs new file mode 100644 index 000000000..4d262715e --- /dev/null +++ b/src/Discord.Net/Extensions/DiscordClientExtensions.cs @@ -0,0 +1,14 @@ +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Extensions +{ + public static class DiscordClientExtensions + { + public static async Task GetOptimalVoiceRegion(this DiscordClient discord) + { + var regions = await discord.GetVoiceRegions().ConfigureAwait(false); + return regions.FirstOrDefault(x => x.IsOptimal); + } + } +} diff --git a/src/Discord.Net/EventExtensions.cs b/src/Discord.Net/Extensions/EventExtensions.cs similarity index 98% rename from src/Discord.Net/EventExtensions.cs rename to src/Discord.Net/Extensions/EventExtensions.cs index b46cb9056..65ab6cebe 100644 --- a/src/Discord.Net/EventExtensions.cs +++ b/src/Discord.Net/Extensions/EventExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace Discord +namespace Discord.Extensions { internal static class EventExtensions { diff --git a/src/Discord.Net/Extensions/GuildExtensions.cs b/src/Discord.Net/Extensions/GuildExtensions.cs new file mode 100644 index 000000000..a438994c9 --- /dev/null +++ b/src/Discord.Net/Extensions/GuildExtensions.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Discord.Extensions +{ + public static class GuildExtensions + { + public static async Task GetTextChannel(this IGuild guild, ulong id) + => await guild.GetChannel(id).ConfigureAwait(false) as ITextChannel; + public static async Task GetVoiceChannel(this IGuild guild, ulong id) + => await guild.GetChannel(id).ConfigureAwait(false) as IVoiceChannel; + } +} diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 21c3c477c..eb89c8452 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,6 +1,4 @@ using Discord.API; -using Discord.Net.Queue; -using Discord.WebSocket.Data; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -14,10 +12,7 @@ namespace Discord ConnectionState ConnectionState { get; } DiscordApiClient ApiClient { get; } - IRequestQueue RequestQueue { get; } - IDataStore DataStore { get; } - - Task Login(string email, string password); + Task Login(TokenType tokenType, string token, bool validateToken = true); Task Logout(); @@ -25,12 +20,12 @@ namespace Discord Task Disconnect(); Task GetChannel(ulong id); - Task> GetDMChannels(); + Task> GetDMChannels(); - Task> GetConnections(); + Task> GetConnections(); Task GetGuild(ulong id); - Task> GetGuilds(); + Task> GetGuilds(); Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); Task GetInvite(string inviteIdOrXkcd); @@ -38,9 +33,9 @@ namespace Discord Task GetUser(ulong id); Task GetUser(string username, ushort discriminator); Task GetCurrentUser(); - Task> QueryUsers(string query, int limit); + Task> QueryUsers(string query, int limit); - Task> GetVoiceRegions(); + Task> GetVoiceRegions(); Task GetVoiceRegion(string id); } } diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 5e5d819b7..83e82824c 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Extensions; +using System; using System.Threading.Tasks; namespace Discord.Logging @@ -9,7 +10,7 @@ namespace Discord.Logging public event Func Message; - internal LogManager(LogSeverity minSeverity) + public LogManager(LogSeverity minSeverity) { Level = minSeverity; } @@ -110,6 +111,6 @@ namespace Discord.Logging Task ILogger.Debug(Exception ex) => Log(LogSeverity.Debug, "Discord", ex); - internal Logger CreateLogger(string name) => new Logger(this, name); + public Logger CreateLogger(string name) => new Logger(this, name); } } diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 74435e012..759917488 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -10,7 +10,7 @@ namespace Discord.Logging public string Name { get; } public LogSeverity Level => _manager.Level; - internal Logger(LogManager manager, string name) + public Logger(LogManager manager, string name) { _manager = manager; Name = name; diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 678dc83cd..0149d130e 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -11,7 +11,8 @@ namespace Discord.Net.Converters public class DiscordContractResolver : DefaultContractResolver { private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); - + private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); @@ -54,12 +55,15 @@ namespace Discord.Net.Converters converter = ImageConverter.Instance; else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) { - var lambda = (Func)propInfo.GetMethod.CreateDelegate(typeof(Func)); - /*var parentArg = Expression.Parameter(typeof(object)); - var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); - var isSpecified = Expression.Property(optional, OptionalConverter.IsSpecifiedProperty); - var lambda = Expression.Lambda>(isSpecified, parentArg).Compile();*/ - property.ShouldSerialize = x => lambda(x); + var typeInput = propInfo.DeclaringType; + var typeOutput = propInfo.PropertyType; + + var getter = typeof(Func<,>).MakeGenericType(typeInput, typeOutput); + var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, typeOutput); + var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); + + property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); converter = OptionalConverter.Instance; } } @@ -73,5 +77,11 @@ namespace Discord.Net.Converters return property; } + + private static bool ShouldSerialize(object owner, Delegate getter) + where TValue : IOptional + { + return (getter as Func)((TOwner)owner).IsSpecified; + } } } diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs index aa1abe9e2..acc583c5f 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -1,14 +1,11 @@ -using Discord.API; -using Newtonsoft.Json; +using Newtonsoft.Json; using System; -using System.Reflection; namespace Discord.Net.Converters { public class OptionalConverter : JsonConverter { public static readonly OptionalConverter Instance = new OptionalConverter(); - internal static readonly PropertyInfo IsSpecifiedProperty = typeof(IOptional).GetTypeInfo().GetDeclaredProperty(nameof(IOptional.IsSpecified)); public override bool CanConvert(Type objectType) => true; public override bool CanRead => false; diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index 013db818a..d18d81abf 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -6,11 +6,13 @@ namespace Discord.Net public class HttpException : Exception { public HttpStatusCode StatusCode { get; } + public string Reason { get; } - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {(int)statusCode} ({statusCode})") + public HttpException(HttpStatusCode statusCode, string reason = null) + : base($"The server responded with error {(int)statusCode} ({statusCode}){(reason != null ? $": \"{reason}\"" : "")}") { StatusCode = statusCode; + Reason = reason; } } } diff --git a/src/Discord.Net/Net/Queue/BucketDefinition.cs b/src/Discord.Net/Net/Queue/BucketDefinition.cs new file mode 100644 index 000000000..64292213d --- /dev/null +++ b/src/Discord.Net/Net/Queue/BucketDefinition.cs @@ -0,0 +1,16 @@ +namespace Discord.Net.Queue +{ + internal struct BucketDefinition + { + public int WindowCount { get; } + public int WindowSeconds { get; } + public GlobalBucket? Parent { get; } + + public BucketDefinition(int windowCount, int windowSeconds, GlobalBucket? parent = null) + { + WindowCount = windowCount; + WindowSeconds = windowSeconds; + Parent = parent; + } + } +} diff --git a/src/Discord.Net/Net/Queue/BucketGroup.cs b/src/Discord.Net/Net/Queue/BucketGroup.cs index 161f08432..0b0367065 100644 --- a/src/Discord.Net/Net/Queue/BucketGroup.cs +++ b/src/Discord.Net/Net/Queue/BucketGroup.cs @@ -1,6 +1,6 @@ namespace Discord.Net.Queue { - internal enum BucketGroup + public enum BucketGroup { Global, Guild diff --git a/src/Discord.Net/Net/Queue/GlobalBucket.cs b/src/Discord.Net/Net/Queue/GlobalBucket.cs index d1e011ffd..7d4ebb761 100644 --- a/src/Discord.Net/Net/Queue/GlobalBucket.cs +++ b/src/Discord.Net/Net/Queue/GlobalBucket.cs @@ -2,11 +2,10 @@ { public enum GlobalBucket { - General, - Login, + GeneralRest, DirectMessage, SendEditMessage, - Gateway, + GeneralGateway, UpdateStatus } } diff --git a/src/Discord.Net/Net/Queue/IQueuedRequest.cs b/src/Discord.Net/Net/Queue/IQueuedRequest.cs index e5575046e..099e0e7ed 100644 --- a/src/Discord.Net/Net/Queue/IQueuedRequest.cs +++ b/src/Discord.Net/Net/Queue/IQueuedRequest.cs @@ -4,10 +4,13 @@ using System.Threading.Tasks; namespace Discord.Net.Queue { + //TODO: Allow user-supplied canceltoken + //TODO: Allow specifying timeout via DiscordApiClient internal interface IQueuedRequest { - TaskCompletionSource Promise { get; } CancellationToken CancelToken { get; } + int? TimeoutTick { get; } + Task Send(); } } diff --git a/src/Discord.Net/Net/Queue/IRequestQueue.cs b/src/Discord.Net/Net/Queue/IRequestQueue.cs deleted file mode 100644 index 75a820934..000000000 --- a/src/Discord.Net/Net/Queue/IRequestQueue.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord.Net.Queue -{ - //TODO: Add docstrings - public interface IRequestQueue - { - Task Clear(GlobalBucket type); - Task Clear(GuildBucket type, ulong guildId); - } -} diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index 365ebfb68..adf54af9c 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -1,25 +1,61 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Queue { - public class RequestQueue : IRequestQueue + public class RequestQueue { + private readonly static ImmutableDictionary _globalLimits; + private readonly static ImmutableDictionary _guildLimits; private readonly SemaphoreSlim _lock; private readonly RequestQueueBucket[] _globalBuckets; - private readonly Dictionary[] _guildBuckets; + private readonly ConcurrentDictionary[] _guildBuckets; private CancellationTokenSource _clearToken; private CancellationToken _parentToken; private CancellationToken _cancelToken; + static RequestQueue() + { + _globalLimits = new Dictionary + { + //REST + [GlobalBucket.GeneralRest] = new BucketDefinition(0, 0), + //[GlobalBucket.Login] = new BucketDefinition(1, 1), + [GlobalBucket.DirectMessage] = new BucketDefinition(5, 5), + [GlobalBucket.SendEditMessage] = new BucketDefinition(50, 10), + + //Gateway + [GlobalBucket.GeneralGateway] = new BucketDefinition(120, 60), + [GlobalBucket.UpdateStatus] = new BucketDefinition(5, 1, GlobalBucket.GeneralGateway) + }.ToImmutableDictionary(); + + _guildLimits = new Dictionary + { + //REST + [GuildBucket.SendEditMessage] = new BucketDefinition(5, 5, GlobalBucket.SendEditMessage), + [GuildBucket.DeleteMessage] = new BucketDefinition(5, 1), + [GuildBucket.DeleteMessages] = new BucketDefinition(1, 1), + [GuildBucket.ModifyMember] = new BucketDefinition(10, 10), + [GuildBucket.Nickname] = new BucketDefinition(1, 1) + }.ToImmutableDictionary(); + } + public RequestQueue() { _lock = new SemaphoreSlim(1, 1); - _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; - _guildBuckets = new Dictionary[Enum.GetValues(typeof(GuildBucket)).Length]; + + _globalBuckets = new RequestQueueBucket[_globalLimits.Count]; + foreach (var pair in _globalLimits) + _globalBuckets[(int)pair.Key] = CreateBucket(pair.Value); + + _guildBuckets = new ConcurrentDictionary[_guildLimits.Count]; + for (int i = 0; i < _guildLimits.Count; i++) + _guildBuckets[i] = new ConcurrentDictionary(); _clearToken = new CancellationTokenSource(); _cancelToken = CancellationToken.None; @@ -27,71 +63,39 @@ namespace Discord.Net.Queue } public async Task SetCancelToken(CancellationToken cancelToken) { - await Lock().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); try { _parentToken = cancelToken; _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; } - finally { Unlock(); } + finally { _lock.Release(); } } - internal Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) { request.CancelToken = _cancelToken; - return Send(request as IQueuedRequest, group, bucketId, guildId); + var bucket = GetBucket(group, bucketId, guildId); + return await bucket.Send(request).ConfigureAwait(false); } - internal Task Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) { request.CancelToken = _cancelToken; - return Send(request as IQueuedRequest, group, bucketId, guildId); + var bucket = GetBucket(group, bucketId, guildId); + return await bucket.Send(request).ConfigureAwait(false); } - private async Task Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) + + private RequestQueueBucket CreateBucket(BucketDefinition def) { - RequestQueueBucket bucket; - - await Lock().ConfigureAwait(false); - try - { - bucket = GetBucket(group, bucketId, guildId); - bucket.Queue(request); - } - finally { Unlock(); } - - //There is a chance the bucket will send this request on its own, but this will simply become a noop then. - var _ = bucket.ProcessQueue(acquireLock: true).ConfigureAwait(false); - - return await request.Promise.Task.ConfigureAwait(false); + var parent = def.Parent != null ? GetGlobalBucket(def.Parent.Value) : null; + return new RequestQueueBucket(def.WindowCount, def.WindowSeconds * 1000, parent); } - private RequestQueueBucket CreateBucket(GlobalBucket bucket) - { - switch (bucket) - { - //Globals - 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)); - } - } - private RequestQueueBucket CreateBucket(GuildBucket bucket, ulong guildId) + public void DestroyGuildBucket(GuildBucket type, ulong guildId) { - switch (bucket) - { - //Per Guild - 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? - case GuildBucket.Nickname: return new RequestQueueBucket(this, bucket, guildId, 1, 1); - - default: throw new ArgumentException($"Unknown guild bucket: {bucket}", nameof(bucket)); - } + //Assume this object is locked + RequestQueueBucket bucket; + _guildBuckets[(int)type].TryRemove(guildId, out bucket); } private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong guildId) @@ -108,58 +112,16 @@ namespace Discord.Net.Queue } private RequestQueueBucket GetGlobalBucket(GlobalBucket type) { - var bucket = _globalBuckets[(int)type]; - if (bucket == null) - { - bucket = CreateBucket(type); - _globalBuckets[(int)type] = bucket; - } - return bucket; + return _globalBuckets[(int)type]; } private RequestQueueBucket GetGuildBucket(GuildBucket type, ulong guildId) { - var bucketGroup = _guildBuckets[(int)type]; - if (bucketGroup == null) - { - bucketGroup = new Dictionary(); - _guildBuckets[(int)type] = bucketGroup; - } - RequestQueueBucket bucket; - if (!bucketGroup.TryGetValue(guildId, out bucket)) - { - bucket = CreateBucket(type, guildId); - bucketGroup[guildId] = bucket; - } - return bucket; - } - - public void DestroyGlobalBucket(GlobalBucket type) - { - //Assume this object is locked - - _globalBuckets[(int)type] = null; - } - public void DestroyGuildBucket(GuildBucket type, ulong guildId) - { - //Assume this object is locked - - var bucketGroup = _guildBuckets[(int)type]; - if (bucketGroup != null) - bucketGroup.Remove(guildId); - } - - public async Task Lock() - { - await _lock.WaitAsync(); - } - public void Unlock() - { - _lock.Release(); + return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type])); } public async Task Clear() { - await Lock().ConfigureAwait(false); + await _lock.WaitAsync().ConfigureAwait(false); try { _clearToken?.Cancel(); @@ -169,37 +131,7 @@ namespace Discord.Net.Queue else _cancelToken = _clearToken.Token; } - finally { Unlock(); } - } - public async Task Clear(GlobalBucket type) - { - var bucket = _globalBuckets[(int)type]; - if (bucket != null) - { - try - { - await bucket.Lock().ConfigureAwait(false); - bucket.Clear(); - } - finally { bucket.Unlock(); } - } - } - public async Task Clear(GuildBucket type, ulong guildId) - { - var bucketGroup = _guildBuckets[(int)type]; - if (bucketGroup != null) - { - RequestQueueBucket bucket; - if (bucketGroup.TryGetValue(guildId, out bucket)) - { - try - { - await bucket.Lock().ConfigureAwait(false); - bucket.Clear(); - } - finally { bucket.Unlock(); } - } - } + finally { _lock.Release(); } } } } diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs index 7b05fb0fe..6fc60d9ea 100644 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -1,191 +1,122 @@ -using System; -using System.Collections.Concurrent; +#pragma warning disable CS4014 +using System; using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; 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 SemaphoreSlim _lock; - private Task _resetTask; - private bool _waitingToProcess; - private int _id; + private readonly int _windowMilliseconds; + private readonly SemaphoreSlim _semaphore; + private readonly object _pauseLock; + private int _pauseEndTick; + private TaskCompletionSource _resumeNotifier; - public int WindowMaxCount { get; } - public int WindowSeconds { get; } - public int WindowCount { get; private set; } + public RequestQueueBucket Parent { get; } + public Task _resetTask { get; } - public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) - : this(parent, windowMaxCount, windowSeconds, chainedBucket) + public RequestQueueBucket(int windowCount, int windowMilliseconds, RequestQueueBucket parent = null) { - _bucketGroup = BucketGroup.Global; - _bucketId = (int)bucket; - _guildId = 0; - } - 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, GlobalBucket? chainedBucket = null) - { - _parent = parent; - WindowMaxCount = windowMaxCount; - WindowSeconds = windowSeconds; - _chainedBucket = chainedBucket; - _queue = new ConcurrentQueue(); - _lock = new SemaphoreSlim(1, 1); - _id = new System.Random().Next(0, int.MaxValue); + if (windowCount != 0) + _semaphore = new SemaphoreSlim(windowCount, windowCount); + _pauseLock = new object(); + _resumeNotifier = new TaskCompletionSource(); + _resumeNotifier.SetResult(0); + _windowMilliseconds = windowMilliseconds; + Parent = parent; } - public void Queue(IQueuedRequest request) + public async Task Send(IQueuedRequest request) { - _queue.Enqueue(request); - } - public async Task ProcessQueue(bool acquireLock = false) - { - //Assume this obj is under lock - - int nextRetry = 1000; - - //If we have another ProcessQueue waiting to run, dont bother with this one - if (_waitingToProcess) return; - _waitingToProcess = true; + var endTick = request.TimeoutTick; - if (acquireLock) - await Lock().ConfigureAwait(false); + //Wait until a spot is open in our bucket + if (_semaphore != null) + await Enter(endTick).ConfigureAwait(false); try { - _waitingToProcess = false; while (true) { - IQueuedRequest request; - - //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort - if (WindowCount == WindowMaxCount) return; - //Get next request, return if queue is empty - if (!_queue.TryPeek(out request)) break; - - try - { - if (request.CancelToken.IsCancellationRequested) - request.Promise.SetException(new OperationCanceledException(request.CancelToken)); - else - { - Stream stream = await request.Send().ConfigureAwait(false); - request.Promise.SetResult(stream); - } - } - catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own - { - WindowCount = WindowMaxCount; - var task = _resetTask; - if (task != null) - { - var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); - await task.ConfigureAwait(false); - int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); - _resetTask = ResetAfter(millis); - } - else - _resetTask = ResetAfter(ex.RetryAfterMilliseconds); - return; - } - catch (HttpException ex) + //Get our 429 state + Task notifier; + int resumeTime; + lock (_pauseLock) { - if (ex.StatusCode == HttpStatusCode.BadGateway) //Gateway unavailable, retry - { - await Task.Delay(nextRetry).ConfigureAwait(false); - nextRetry *= 2; - if (nextRetry > 30000) - nextRetry = 30000; - continue; - } - else - { - //We dont need to throw this here, pass the exception via the promise - request.Promise.SetException(ex); - } + notifier = _resumeNotifier.Task; + resumeTime = _pauseEndTick; } - //Request completed or had an error other than 429 - _queue.TryDequeue(out request); - WindowCount++; - nextRetry = 1000; - - if (WindowCount == 1 && WindowSeconds > 0) + //Are we paused due to a 429? + if (!notifier.IsCompleted) { - //First request for this window, schedule a reset - _resetTask = ResetAfter(WindowSeconds * 1000); + //If the 429 ends after the maximum time for this request, timeout immediately + if (endTick.HasValue && endTick.Value < resumeTime) + throw new TimeoutException(); + + //Wait for the 429 to complete + await notifier.ConfigureAwait(false); } - } - //If queue is empty, non-global, and there is no active rate limit, remove this bucket - if (_resetTask == null && _bucketGroup == BucketGroup.Guild) - { try { - await _parent.Lock().ConfigureAwait(false); - if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks - _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); + //If there's a parent bucket, pass this request to them + if (Parent != null) + return await Parent.Send(request).ConfigureAwait(false); + + //We have all our semaphores, send the request + return await request.Send().ConfigureAwait(false); } - finally + catch (HttpRateLimitException ex) { - _parent.Unlock(); + Pause(ex.RetryAfterMilliseconds); + continue; } } } finally { - if (acquireLock) - Unlock(); + //Make sure we put this entry back after WindowMilliseconds + if (_semaphore != null) + QueueExit(); } } - public void Clear() - { - //Assume this obj is under lock - IQueuedRequest request; - - while (_queue.TryDequeue(out request)) { } - } - private async Task ResetAfter(int milliseconds) + private void Pause(int milliseconds) { - if (milliseconds > 0) - await Task.Delay(milliseconds).ConfigureAwait(false); - try + lock (_pauseLock) { - await Lock().ConfigureAwait(false); - - //Reset the current window count and set our state back to normal - WindowCount = 0; - _resetTask = null; - - //Wait is over, work through the current queue - await ProcessQueue().ConfigureAwait(false); + //If we aren't already waiting on a 429's time, create a new notifier task + if (_resumeNotifier.Task.IsCompleted) + { + _resumeNotifier = new TaskCompletionSource(); + _pauseEndTick = unchecked(Environment.TickCount + milliseconds); + QueueResume(milliseconds); + } } - finally { Unlock(); } + } + private async Task QueueResume(int millis) + { + await Task.Delay(millis).ConfigureAwait(false); + _resumeNotifier.SetResult(0); } - public async Task Lock() + private async Task Enter(int? endTick) { - await _lock.WaitAsync(); + if (endTick.HasValue) + { + int millis = unchecked(Environment.TickCount - endTick.Value); + if (millis <= 0 || !await _semaphore.WaitAsync(millis).ConfigureAwait(false)) + throw new TimeoutException(); + } + else + await _semaphore.WaitAsync().ConfigureAwait(false); } - public void Unlock() + private async Task QueueExit() { - _lock.Release(); + await Task.Delay(_windowMilliseconds).ConfigureAwait(false); + _semaphore.Release(); } } } diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs index 7c71d114a..a37204f54 100644 --- a/src/Discord.Net/Net/Queue/RestRequest.cs +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -1,4 +1,5 @@ using Discord.Net.Rest; +using System; using System.Collections.Generic; using System.IO; using System.Threading; @@ -13,25 +14,26 @@ namespace Discord.Net.Queue public string Endpoint { get; } public string Json { get; } public bool HeaderOnly { get; } + public int? TimeoutTick { get; } public IReadOnlyDictionary MultipartParams { get; } public TaskCompletionSource Promise { get; } public CancellationToken CancelToken { get; set; } public bool IsMultipart => MultipartParams != null; - public RestRequest(IRestClient client, string method, string endpoint, string json, bool headerOnly) - : this(client, method, endpoint, headerOnly) + public RestRequest(IRestClient client, string method, string endpoint, string json, bool headerOnly, RequestOptions options) + : this(client, method, endpoint, headerOnly, options) { Json = json; } - public RestRequest(IRestClient client, string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) - : this(client, method, endpoint, headerOnly) + public RestRequest(IRestClient client, string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly, RequestOptions options) + : this(client, method, endpoint, headerOnly, options) { MultipartParams = multipartParams; } - private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly) + private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly, RequestOptions options) { Client = client; Method = method; @@ -39,6 +41,7 @@ namespace Discord.Net.Queue Json = null; MultipartParams = null; HeaderOnly = headerOnly; + TimeoutTick = options.Timeout.HasValue ? (int?)unchecked(Environment.TickCount + options.Timeout.Value) : null; Promise = new TaskCompletionSource(); } diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs index bd8f492c3..003bf731f 100644 --- a/src/Discord.Net/Net/Queue/WebSocketRequest.cs +++ b/src/Discord.Net/Net/Queue/WebSocketRequest.cs @@ -1,4 +1,5 @@ using Discord.Net.WebSockets; +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -12,17 +13,22 @@ namespace Discord.Net.Queue public int DataIndex { get; } public int DataCount { get; } public bool IsText { get; } + public int? TimeoutTick { get; } public TaskCompletionSource Promise { get; } public CancellationToken CancelToken { get; set; } - public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText) : this(client, data, 0, data.Length, isText) { } - public WebSocketRequest(IWebSocketClient client, byte[] data, int index, int count, bool isText) + public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, RequestOptions options) : this(client, data, 0, data.Length, isText, options) { } + public WebSocketRequest(IWebSocketClient client, byte[] data, int index, int count, bool isText, RequestOptions options) { Client = client; Data = data; DataIndex = index; DataCount = count; IsText = isText; + if (options != null) + TimeoutTick = unchecked(Environment.TickCount + options.Timeout.Value); + else + TimeoutTick = null; Promise = new TaskCompletionSource(); } diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 9d83e9a32..088c09f87 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -1,4 +1,6 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,15 +13,16 @@ using System.Threading.Tasks; namespace Discord.Net.Rest { - public class DefaultRestClient : IRestClient + public sealed class DefaultRestClient : IRestClient { private const int HR_SECURECHANNELFAILED = -2146233079; - protected readonly HttpClient _client; - protected readonly string _baseUrl; + private readonly HttpClient _client; + private readonly string _baseUrl; + private readonly JsonSerializer _errorDeserializer; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; - protected bool _isDisposed; + private bool _isDisposed; public DefaultRestClient(string baseUrl) { @@ -29,16 +32,16 @@ namespace Discord.Net.Rest { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, - UseProxy = false, - PreAuthenticate = false + UseProxy = false }); SetHeader("accept-encoding", "gzip, deflate"); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; + _errorDeserializer = new JsonSerializer(); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { @@ -141,8 +144,23 @@ namespace Discord.Net.Rest if (statusCode < 200 || statusCode >= 300) //2xx = Success { if (statusCode == 429) + { + //TODO: Include bucket info throw new HttpRateLimitException(int.Parse(response.Headers.GetValues("retry-after").First())); - throw new HttpException(response.StatusCode); + } + + string reason = null; + try + { + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = new StreamReader(stream)) + using (var json = new JsonTextReader(reader)) + { + reason = (_errorDeserializer.Deserialize(json) as JToken).Value("message"); + } + } + catch { } //Might have been HTML + throw new HttpException(response.StatusCode, reason); } if (headerOnly) diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index d9559a2cf..03c965bf5 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Extensions; +using System; using System.ComponentModel; using System.IO; using System.Net.WebSockets; diff --git a/src/Discord.Net/RequestOptions.cs b/src/Discord.Net/RequestOptions.cs new file mode 100644 index 000000000..16b5b4d76 --- /dev/null +++ b/src/Discord.Net/RequestOptions.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public class RequestOptions + { + /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. + public int? Timeout { get; set; } + } +} diff --git a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs deleted file mode 100644 index 0efa29da3..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class DMChannel : IDMChannel - { - /// - public ulong Id { get; } - internal DiscordClient Discord { get; } - - /// - public User Recipient { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - - internal DMChannel(DiscordClient discord, Model model) - { - Id = model.Id; - Discord = discord; - - Update(model); - } - private void Update(Model model) - { - if (Recipient == null) - Recipient = new PublicUser(Discord, model.Recipient); - else - Recipient.Update(model.Recipient); - } - - /// - public async Task GetUser(ulong id) - { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - if (id == Recipient.Id) - return Recipient; - else if (id == currentUser.Id) - return currentUser; - else - return null; - } - /// - public async Task> GetUsers() - { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - return ImmutableArray.Create(currentUser, Recipient); - } - - /// - public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, x)); - } - /// - public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, x)); - } - - /// - public async Task SendMessage(string text, bool isTTS = false) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); - return new Message(this, model); - } - /// - public async Task SendFile(string filePath, string text = null, bool isTTS = false) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); - return new Message(this, model); - } - } - /// - public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); - return new Message(this, model); - } - - /// - public async Task DeleteMessages(IEnumerable messages) - { - await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - - /// - public async Task TriggerTyping() - { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); - } - - /// - public async Task Close() - { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); - } - - /// - public async Task Update() - { - var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); - Update(model); - } - - /// - public override string ToString() => '@' + Recipient.ToString(); - private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - - IUser IDMChannel.Recipient => Recipient; - IEnumerable IMessageChannel.CachedMessages => Array.Empty(); - - async Task> IChannel.GetUsers() - => await GetUsers().ConfigureAwait(false); - async Task> IChannel.GetUsers(int limit, int offset) - => (await GetUsers().ConfigureAwait(false)).Skip(offset).Take(limit); - async Task IChannel.GetUser(ulong id) - => await GetUser(id).ConfigureAwait(false); - Task IMessageChannel.GetCachedMessage(ulong id) - => Task.FromResult(null); - async Task> IMessageChannel.GetMessages(int limit) - => await GetMessages(limit).ConfigureAwait(false); - async Task> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit) - => await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false); - async Task IMessageChannel.SendMessage(string text, bool isTTS) - => await SendMessage(text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(string filePath, string text, bool isTTS) - => await SendFile(filePath, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS) - => await SendFile(stream, filename, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.TriggerTyping() - => await TriggerTyping().ConfigureAwait(false); - } -} diff --git a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs deleted file mode 100644 index 4c171bea2..000000000 --- a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class TextChannel : GuildChannel, ITextChannel - { - /// - public string Topic { get; private set; } - - /// - public string Mention => MentionUtils.Mention(this); - - internal TextChannel(Guild guild, Model model) - : base(guild, model) - { - } - - internal override void Update(Model model) - { - Topic = model.Topic; - base.Update(model); - } - - public async Task Modify(Action func) - { - if (func != null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyTextChannelParams(); - func(args); - var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - Update(model); - } - - /// Gets a user in this channel with the given id. - public async Task GetUser(ulong id) - { - var user = await Guild.GetUser(id).ConfigureAwait(false); - if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) - return user; - return null; - } - /// Gets all users in this channel. - public async Task> GetUsers() - { - var users = await Guild.GetUsers().ConfigureAwait(false); - return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)); - } - /// Gets a paginated collection of users in this channel. - public async Task> GetUsers(int limit, int offset) - { - var users = await Guild.GetUsers(limit, offset).ConfigureAwait(false); - return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)); - } - - /// - public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, x)); - } - /// - public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - { - var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, x)); - } - - /// - public async Task SendMessage(string text, bool isTTS = false) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); - return new Message(this, model); - } - /// - public async Task SendFile(string filePath, string text = null, bool isTTS = false) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); - return new Message(this, model); - } - } - /// - public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); - return new Message(this, model); - } - - /// - public async Task DeleteMessages(IEnumerable messages) - { - await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - - /// - public async Task TriggerTyping() - { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); - } - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - - - protected override Task GetUserInternal(ulong id) => GetUser(id); - protected override Task> GetUsersInternal() => GetUsers(); - protected override Task> GetUsersInternal(int limit, int offset) => GetUsers(limit, offset); - - IEnumerable IMessageChannel.CachedMessages => Array.Empty(); - - Task IMessageChannel.GetCachedMessage(ulong id) - => Task.FromResult(null); - async Task> IMessageChannel.GetMessages(int limit) - => await GetMessages(limit).ConfigureAwait(false); - async Task> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit) - => await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false); - async Task IMessageChannel.SendMessage(string text, bool isTTS) - => await SendMessage(text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(string filePath, string text, bool isTTS) - => await SendFile(filePath, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS) - => await SendFile(stream, filename, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.TriggerTyping() - => await TriggerTyping().ConfigureAwait(false); - } -} diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs deleted file mode 100644 index d7f5a3831..000000000 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Diagnostics; -using Model = Discord.API.GuildEmbed; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class GuildEmbed : IGuildEmbed - { - /// - public ulong Id { get; } - /// - public bool IsEnabled { get; private set; } - /// - public ulong? ChannelId { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - - internal GuildEmbed(Model model) - { - Update(model); - } - - private void Update(Model model) - { - ChannelId = model.ChannelId; - IsEnabled = model.Enabled; - } - - public override string ToString() => Id.ToString(); - private string DebuggerDisplay => $"{Id}{(IsEnabled ? " (Enabled)" : "")}"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/PublicUser.cs b/src/Discord.Net/Rest/Entities/Users/PublicUser.cs deleted file mode 100644 index 20f9a3919..000000000 --- a/src/Discord.Net/Rest/Entities/Users/PublicUser.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Model = Discord.API.User; - -namespace Discord.Rest -{ - public class PublicUser : User - { - internal override DiscordClient Discord { get; } - - internal PublicUser(DiscordClient discord, Model model) - : base(model) - { - Discord = discord; - } - } -} diff --git a/src/Discord.Net/ConcurrentHashSet.cs b/src/Discord.Net/Utilities/ConcurrentHashSet.cs similarity index 99% rename from src/Discord.Net/ConcurrentHashSet.cs rename to src/Discord.Net/Utilities/ConcurrentHashSet.cs index 18572cfcf..1805649a9 100644 --- a/src/Discord.Net/ConcurrentHashSet.cs +++ b/src/Discord.Net/Utilities/ConcurrentHashSet.cs @@ -10,7 +10,7 @@ namespace Discord //Based on https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs //Copyright (c) .NET Foundation and Contributors [DebuggerDisplay("Count = {Count}")] - internal class ConcurrentHashSet : IEnumerable + internal class ConcurrentHashSet : IReadOnlyCollection { private sealed class Tables { diff --git a/src/Discord.Net/DateTimeUtils.cs b/src/Discord.Net/Utilities/DateTimeUtils.cs similarity index 100% rename from src/Discord.Net/DateTimeUtils.cs rename to src/Discord.Net/Utilities/DateTimeUtils.cs diff --git a/src/Discord.Net/API/IOptional.cs b/src/Discord.Net/Utilities/IOptional.cs similarity index 81% rename from src/Discord.Net/API/IOptional.cs rename to src/Discord.Net/Utilities/IOptional.cs index 51d4f5271..47c078b66 100644 --- a/src/Discord.Net/API/IOptional.cs +++ b/src/Discord.Net/Utilities/IOptional.cs @@ -1,4 +1,4 @@ -namespace Discord.API +namespace Discord { public interface IOptional { diff --git a/src/Discord.Net/MentionUtils.cs b/src/Discord.Net/Utilities/MentionUtils.cs similarity index 94% rename from src/Discord.Net/MentionUtils.cs rename to src/Discord.Net/Utilities/MentionUtils.cs index 7d37ffc58..636711552 100644 --- a/src/Discord.Net/MentionUtils.cs +++ b/src/Discord.Net/Utilities/MentionUtils.cs @@ -64,11 +64,11 @@ namespace Discord } /// Gets the ids of all users mentioned in a provided text. - public static IImmutableList GetUserMentions(string text) => GetMentions(text, _userRegex).ToImmutableArray(); + public static ImmutableArray GetUserMentions(string text) => GetMentions(text, _userRegex).ToImmutable(); /// Gets the ids of all channels mentioned in a provided text. - public static IImmutableList GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToImmutableArray(); + public static ImmutableArray GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToImmutable(); /// Gets the ids of all roles mentioned in a provided text. - public static IImmutableList GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToImmutableArray(); + public static ImmutableArray GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToImmutable(); private static ImmutableArray.Builder GetMentions(string text, Regex regex) { var matches = regex.Matches(text); diff --git a/src/Discord.Net/WebSocket/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs similarity index 57% rename from src/Discord.Net/WebSocket/MessageCache.cs rename to src/Discord.Net/Utilities/MessageCache.cs index 5051efc3a..4b1a35d08 100644 --- a/src/Discord.Net/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -6,57 +7,58 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -namespace Discord.WebSocket +namespace Discord { internal class MessageCache { - private readonly DiscordClient _discord; - private readonly IMessageChannel _channel; - private readonly ConcurrentDictionary _messages; + private readonly DiscordSocketClient _discord; + private readonly ICachedMessageChannel _channel; + private readonly ConcurrentDictionary _messages; private readonly ConcurrentQueue _orderedMessages; private readonly int _size; - public IEnumerable Messages => _messages.Select(x => x.Value); + public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); - public MessageCache(DiscordClient discord, IMessageChannel channel) + public MessageCache(DiscordSocketClient discord, ICachedMessageChannel channel) { _discord = discord; _channel = channel; _size = discord.MessageCacheSize; - _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); + _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); _orderedMessages = new ConcurrentQueue(); } - internal void Add(Message message) + public void Add(CachedMessage message) { if (_messages.TryAdd(message.Id, message)) { _orderedMessages.Enqueue(message.Id); ulong msgId; - Message msg; + CachedMessage msg; while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) _messages.TryRemove(msgId, out msg); } } - internal void Remove(ulong id) + public CachedMessage Remove(ulong id) { - Message msg; + CachedMessage msg; _messages.TryRemove(id, out msg); + return msg; } - public Message Get(ulong id) + public CachedMessage Get(ulong id) { - Message result; + CachedMessage result; if (_messages.TryGetValue(id, out result)) return result; return null; } - public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; + if (limit == 0) return ImmutableArray.Empty; IEnumerable cachedMessageIds; if (fromMessageId == null) @@ -70,27 +72,36 @@ namespace Discord.WebSocket .Take(limit) .Select(x => { - Message msg; + CachedMessage msg; if (_messages.TryGetValue(x, out msg)) - return msg; + return msg as CachedMessage; return null; }) .Where(x => x != null) .ToImmutableArray(); } - public async Task> Download(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public async Task Download(ulong id) { - //TODO: Test heavily - + var msg = Get(id); + if (msg != null) + return msg; + var model = await _discord.ApiClient.GetChannelMessage(_channel.Id, id).ConfigureAwait(false); + if (model != null) + return new CachedMessage(_channel, new User(_discord, model.Author), model); + return null; + } + public async Task> Download(ulong? fromId, Direction dir, int limit) + { + //TODO: Test heavily, especially the ordering of messages if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; + if (limit == 0) return ImmutableArray.Empty; - var cachedMessages = GetMany(fromMessageId, dir, limit); + var cachedMessages = GetMany(fromId, dir, limit); if (cachedMessages.Count == limit) return cachedMessages; else if (cachedMessages.Count > limit) - return cachedMessages.Skip(cachedMessages.Count - limit); + return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); else { var args = new GetChannelMessagesParams @@ -100,8 +111,7 @@ namespace Discord.WebSocket RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id }; var downloadedMessages = await _discord.ApiClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false); - //TODO: Ugly channel cast - return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, (_channel as Channel).GetUser(x.Id), x))).ToImmutableArray(); + return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetCachedUser(x.Id), x))).ToImmutableArray(); } } } diff --git a/src/Discord.Net/API/Optional.cs b/src/Discord.Net/Utilities/Optional.cs similarity index 98% rename from src/Discord.Net/API/Optional.cs rename to src/Discord.Net/Utilities/Optional.cs index b828608b2..095e54bda 100644 --- a/src/Discord.Net/API/Optional.cs +++ b/src/Discord.Net/Utilities/Optional.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace Discord.API +namespace Discord { //Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs [DebuggerDisplay(@"{DebuggerDisplay,nq}")] diff --git a/src/Discord.Net/Preconditions.cs b/src/Discord.Net/Utilities/Preconditions.cs similarity index 100% rename from src/Discord.Net/Preconditions.cs rename to src/Discord.Net/Utilities/Preconditions.cs diff --git a/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs b/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs deleted file mode 100644 index 0b1c78317..000000000 --- a/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 28a4ca0d1..000000000 --- a/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.WebSocket.Data -{ - public class DefaultDataStore : IDataStore - { - private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 - 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)); - _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 deleted file mode 100644 index b980d13d5..000000000 --- a/src/Discord.Net/WebSocket/Data/IDataStore.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.WebSocket.Data -{ - public interface IDataStore - { - IEnumerable Channels { get; } - IEnumerable Guilds { 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); - - 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 deleted file mode 100644 index 8512a2679..000000000 --- a/src/Discord.Net/WebSocket/Data/SharedDataStore.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 911420731..000000000 --- a/src/Discord.Net/WebSocket/DiscordClient.cs +++ /dev/null @@ -1,889 +0,0 @@ -using Discord.API; -using Discord.API.Gateway; -using Discord.API.Rest; -using Discord.Logging; -using Discord.Net; -using Discord.Net.Converters; -using Discord.Net.Queue; -using Discord.Net.WebSockets; -using Discord.WebSocket.Data; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -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; - -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 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, _restLogger, _gatewayLogger; - private readonly SemaphoreSlim _connectionLock; - 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 ImmutableDictionary _voiceRegions; - private string _sessionId; - private bool _isDisposed; - - public int ShardId { get; } - 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; } - internal int MessageCacheSize { get; private set; } - internal bool UsePermissionCache { get; private set; } - - 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(); - - ShardId = config.ShardId; - _totalShards = config.TotalShards; - - _connectionTimeout = config.ConnectionTimeout; - _reconnectDelay = config.ReconnectDelay; - _failedReconnectDelay = config.FailedReconnectDelay; - _dataStoreProvider = config.DataStoreProvider; - - MessageCacheSize = config.MessageCacheSize; - UsePermissionCache = config.UsePermissionsCache; - _enablePreUpdateEvents = config.EnablePreUpdateEvents; - _largeThreshold = config.LargeThreshold; - - _log = new LogManager(config.LogLevel); - _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); - _discordLogger = _log.CreateLogger("Discord"); - _restLogger = _log.CreateLogger("Rest"); - _gatewayLogger = _log.CreateLogger("Gateway"); - - _connectionLock = new SemaphoreSlim(1, 1); - _requestQueue = new RequestQueue(); - _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - - ApiClient = new API.DiscordApiClient(config.RestClientProvider, config.WebSocketProvider, _serializer, _requestQueue); - ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.Verbose($"{method} {endpoint}: {millis} ms"); - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}"); - ApiClient.ReceivedGatewayEvent += ProcessMessage; - GatewaySocket = config.WebSocketProvider(); - - _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(TokenType.User, null, email, password, true, false).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - public async Task Login(TokenType tokenType, string token, bool validateToken = true) - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) - { - if (LoginState != LoginState.LoggedOut) - await LogoutInternal().ConfigureAwait(false); - LoginState = LoginState.LoggingIn; - - try - { - 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); - } - catch (HttpException ex) - { - throw new ArgumentException("Token validation failed", nameof(token), ex); - } - } - - var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); - - LoginState = LoginState.LoggedIn; - } - catch (Exception) - { - await LogoutInternal().ConfigureAwait(false); - throw; - } - - await LoggedIn.Raise().ConfigureAwait(false); - } - - public async Task Logout() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await LogoutInternal().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task LogoutInternal() - { - if (LoginState == LoginState.LoggedOut) return; - LoginState = LoginState.LoggingOut; - - if (ConnectionState != ConnectionState.Disconnected) - await DisconnectInternal().ConfigureAwait(false); - - await ApiClient.Logout().ConfigureAwait(false); - - _voiceRegions = ImmutableDictionary.Create(); - CurrentUser = null; - - LoginState = LoginState.LoggedOut; - - await LoggedOut.Raise().ConfigureAwait(false); - } - - public async Task Connect() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternal().ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - } - private async Task ConnectInternal() - { - if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting."); - - ConnectionState = ConnectionState.Connecting; - try - { - await ApiClient.Connect().ConfigureAwait(false); - - ConnectionState = ConnectionState.Connected; - } - catch (Exception) - { - await DisconnectInternal().ConfigureAwait(false); - throw; - } - - await Connected.Raise().ConfigureAwait(false); - } - - public async Task Disconnect() - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - 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() - { - var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); - return models.Select(x => new Connection(x)); - } - - public Channel GetChannel(ulong id) - { - return DataStore.GetChannel(id); - } - - public async Task GetInvite(string inviteIdOrXkcd) - { - var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); - if (model != null) - return new Invite(this, model); - return null; - } - - public Guild GetGuild(ulong id) - { - return DataStore.GetGuild(id); - } - public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) - { - var args = new CreateGuildParams(); - var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); - return new Guild(this, model); - } - - public User GetUser(ulong id) - { - return DataStore.GetUser(id); - } - public User GetUser(string username, ushort discriminator) - { - return DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); - } - public async Task> QueryUsers(string query, int limit) - { - var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); - return models.Select(x => new User(this, x)); - } - - public VoiceRegion GetVoiceRegion(string id) - { - VoiceRegion region; - if (_voiceRegions.TryGetValue(id, out region)) - return region; - return null; - } - - private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload) - { - try - { - switch (opCode) - { - case GatewayOpCodes.Dispatch: - switch (type) - { - //Global - case "READY": - { - //TODO: Store guilds even if they're unavailable - //TODO: Make downloading large guilds optional - //TODO: Add support for unavailable guilds - - var data = 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(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($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}"); - break; - - //Others - default: - _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}"); - break; - } - break; - } - } - catch (Exception ex) - { - _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex); - } - } - - Task IDiscordClient.GetChannel(ulong id) - => Task.FromResult(GetChannel(id)); - Task> IDiscordClient.GetDMChannels() - => 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)); - Task> IDiscordClient.GetGuilds() - => 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)); - 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.ToImmutableArray()); - Task IDiscordClient.GetVoiceRegion(string id) - => Task.FromResult(GetVoiceRegion(id)); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs b/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs deleted file mode 100644 index b645d1c20..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.WebSocket -{ - //TODO: Look into Internal abstract pattern - can we get rid of this? - public abstract class Channel : IChannel - { - /// - public ulong Id { get; private set; } - public IEnumerable Users => GetUsersInternal(); - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - - internal Channel(ulong id) - { - Id = id; - } - - /// - public User GetUser(ulong id) - => GetUserInternal(id); - - protected abstract User GetUserInternal(ulong id); - protected abstract IEnumerable GetUsersInternal(); - - Task> IChannel.GetUsers() - => Task.FromResult>(GetUsersInternal()); - Task> IChannel.GetUsers(int limit, int offset) - => Task.FromResult>(GetUsersInternal().Skip(offset).Take(limit)); - Task IChannel.GetUser(ulong id) - => Task.FromResult(GetUserInternal(id)); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs deleted file mode 100644 index bb93566af..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Discord.API.Rest; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class DMChannel : Channel, IDMChannel - { - private readonly MessageCache _messages; - - internal DiscordClient Discord { get; } - - /// - public User Recipient { get; private set; } - - /// - public new IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - public IEnumerable CachedMessages => _messages.Messages; - - internal DMChannel(DiscordClient discord, User recipient, Model model) - : base(model.Id) - { - Discord = discord; - Recipient = recipient; - _messages = new MessageCache(Discord, this); - - Update(model); - } - private void Update(Model model) - { - Recipient.Update(model.Recipient); - } - - protected override User GetUserInternal(ulong id) - { - if (id == Recipient.Id) - return Recipient; - else if (id == Discord.CurrentUser.Id) - return Discord.CurrentUser; - else - return null; - } - protected override IEnumerable GetUsersInternal() - { - return Users; - } - - /// Gets the message from this channel's cache with the given id, or null if none was found. - public Message GetCachedMessage(ulong id) - { - return _messages.Get(id); - } - /// Gets the last N messages from this message channel. - public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); - } - /// Gets a collection of messages in this channel. - public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); - } - - /// - public async Task SendMessage(string text, bool isTTS = false) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Id), model); - } - /// - public async Task SendFile(string filePath, string text = null, bool isTTS = false) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Id), model); - } - } - /// - public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Id), model); - } - - /// - public async Task DeleteMessages(IEnumerable messages) - { - await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - - /// - public async Task TriggerTyping() - { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); - } - - /// - public async Task Close() - { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); - } - - /// - public override string ToString() => '@' + Recipient.ToString(); - private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - - IUser IDMChannel.Recipient => Recipient; - IEnumerable IMessageChannel.CachedMessages => CachedMessages; - - Task> IChannel.GetUsers() - => Task.FromResult>(Users); - Task> IChannel.GetUsers(int limit, int offset) - => Task.FromResult>(Users.Skip(offset).Take(limit)); - Task IChannel.GetUser(ulong id) - => Task.FromResult(GetUser(id)); - Task IMessageChannel.GetCachedMessage(ulong id) - => Task.FromResult(GetCachedMessage(id)); - async Task> IMessageChannel.GetMessages(int limit) - => await GetMessages(limit).ConfigureAwait(false); - async Task> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit) - => await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false); - async Task IMessageChannel.SendMessage(string text, bool isTTS) - => await SendMessage(text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(string filePath, string text, bool isTTS) - => await SendFile(filePath, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS) - => await SendFile(stream, filename, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.TriggerTyping() - => await TriggerTyping().ConfigureAwait(false); - Task IUpdateable.Update() - => Task.CompletedTask; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs deleted file mode 100644 index db571577a..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - public abstract class GuildChannel : Channel, IGuildChannel - { - private ConcurrentDictionary _overwrites; - internal PermissionsCache _permissions; - - /// Gets the guild this channel is a member of. - public Guild Guild { get; } - - /// - public string Name { get; private set; } - /// - public int Position { get; private set; } - public new abstract IEnumerable Users { get; } - - /// - public IReadOnlyDictionary PermissionOverwrites => _overwrites; - internal DiscordClient Discord => Guild.Discord; - - internal GuildChannel(Guild guild, Model model) - : base(model.Id) - { - Guild = guild; - - Update(model); - } - internal virtual void Update(Model model) - { - Name = model.Name; - Position = model.Position; - - var newOverwrites = new ConcurrentDictionary(); - for (int i = 0; i < model.PermissionOverwrites.Length; i++) - { - var overwrite = model.PermissionOverwrites[i]; - newOverwrites[overwrite.TargetId] = new Overwrite(overwrite); - } - _overwrites = newOverwrites; - } - - public async Task Modify(Action func) - { - if (func != null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildChannelParams(); - func(args); - await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - } - - /// Gets a user in this channel with the given id. - public new abstract GuildUser GetUser(ulong id); - protected override User GetUserInternal(ulong id) - { - return GetUser(id).GlobalUser; - } - protected override IEnumerable GetUsersInternal() - { - return Users.Select(x => x.GlobalUser); - } - - /// - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - Overwrite value; - if (_overwrites.TryGetValue(Id, out value)) - return value.Permissions; - return null; - } - /// - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - Overwrite value; - if (_overwrites.TryGetValue(Id, out value)) - return value.Permissions; - return null; - } - /// Downloads a collection of all invites to this channel. - public async Task> GetInvites() - { - var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)); - } - - /// - public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) - { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); - } - /// - public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) - { - var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); - } - /// - public async Task RemovePermissionOverwrite(IUser user) - { - await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); - } - /// - public async Task RemovePermissionOverwrite(IRole role) - { - await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); - } - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the guild after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) - { - var args = new CreateChannelInviteParams - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - Temporary = isTemporary, - XkcdPass = withXkcd - }; - var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); - return new InviteMetadata(Discord, model); - } - - /// - public async Task Delete() - { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); - } - - /// - public override string ToString() => Name; - - IGuild IGuildChannel.Guild => Guild; - async Task IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) - => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); - async Task> IGuildChannel.GetInvites() - => await GetInvites().ConfigureAwait(false); - Task> IGuildChannel.GetUsers() - => Task.FromResult>(Users); - Task IGuildChannel.GetUser(ulong id) - => Task.FromResult(GetUser(id)); - Task> IChannel.GetUsers() - => Task.FromResult>(Users); - Task> IChannel.GetUsers(int limit, int offset) - => Task.FromResult>(Users.Skip(offset).Take(limit)); - Task IChannel.GetUser(ulong id) - => Task.FromResult(GetUser(id)); - Task IUpdateable.Update() - => Task.CompletedTask; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs deleted file mode 100644 index ade45276e..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class TextChannel : GuildChannel, ITextChannel - { - private readonly MessageCache _messages; - - /// - public string Topic { get; private set; } - - /// - public string Mention => MentionUtils.Mention(this); - public override IEnumerable Users - => _permissions.Members.Where(x => x.Permissions.ReadMessages).Select(x => x.User).ToImmutableArray(); - public IEnumerable CachedMessages => _messages.Messages; - - internal TextChannel(Guild guild, Model model) - : base(guild, model) - { - _messages = new MessageCache(Discord, this); - } - - internal override void Update(Model model) - { - Topic = model.Topic; - base.Update(model); - } - - public async Task Modify(Action func) - { - if (func != null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyTextChannelParams(); - func(args); - await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - } - - /// Gets the message from this channel's cache with the given id, or null if none was found. - public Message GetCachedMessage(ulong id) - { - return _messages.Get(id); - } - /// Gets the last N messages from this message channel. - public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); - } - /// Gets a collection of messages in this channel. - public async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - { - return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); - } - - public override GuildUser GetUser(ulong id) - { - var member = _permissions.Get(id); - if (member != null && member.Value.Permissions.ReadMessages) - return member.Value.User; - return null; - } - - /// - public async Task SendMessage(string text, bool isTTS = false) - { - var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Id), model); - } - /// - public async Task SendFile(string filePath, string text = null, bool isTTS = false) - { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Author.Id), model); - } - } - /// - public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) - { - var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); - return new Message(this, GetUser(model.Author.Id), model); - } - - /// - public async Task DeleteMessages(IEnumerable messages) - { - await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); - } - - /// - public async Task TriggerTyping() - { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); - } - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - - IEnumerable IMessageChannel.CachedMessages => CachedMessages; - - Task IMessageChannel.GetCachedMessage(ulong id) - => Task.FromResult(GetCachedMessage(id)); - async Task> IMessageChannel.GetMessages(int limit) - => await GetMessages(limit).ConfigureAwait(false); - async Task> IMessageChannel.GetMessages(ulong fromMessageId, Direction dir, int limit) - => await GetMessages(fromMessageId, dir, limit).ConfigureAwait(false); - async Task IMessageChannel.SendMessage(string text, bool isTTS) - => await SendMessage(text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(string filePath, string text, bool isTTS) - => await SendFile(filePath, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.SendFile(Stream stream, string filename, string text, bool isTTS) - => await SendFile(stream, filename, text, isTTS).ConfigureAwait(false); - async Task IMessageChannel.TriggerTyping() - => await TriggerTyping().ConfigureAwait(false); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs deleted file mode 100644 index d1f374499..000000000 --- a/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Channel; - -namespace Discord.WebSocket -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class VoiceChannel : GuildChannel, IVoiceChannel - { - /// - public int Bitrate { get; private set; } - /// - public int UserLimit { get; private set; } - - public override IEnumerable Users - => Guild.Users.Where(x => x.VoiceChannel == this); - - internal VoiceChannel(Guild guild, Model model) - : base(guild, model) - { - } - internal override void Update(Model model) - { - base.Update(model); - Bitrate = model.Bitrate; - UserLimit = model.UserLimit; - } - - /// - public async Task Modify(Action func) - { - if (func != null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyVoiceChannelParams(); - func(args); - await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); - } - - public override GuildUser GetUser(ulong id) - { - var member = _permissions.Get(id); - if (member != null && member.Value.Permissions.ReadMessages) - return member.Value.User; - return null; - } - - private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs b/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs deleted file mode 100644 index c0663e924..000000000 --- a/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs +++ /dev/null @@ -1,344 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Guild; -using System.Diagnostics; - -namespace Discord.WebSocket -{ - /// Represents a Discord guild (called a server in the official client). - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Guild : IGuild, IUserGuild - { - private ConcurrentHashSet _channels; - private ConcurrentDictionary _members; - private ConcurrentDictionary _roles; - private ulong _ownerId; - private ulong? _afkChannelId, _embedChannelId; - private string _iconId, _splashId; - private int _userCount; - - /// - public ulong Id { get; } - internal DiscordClient Discord { get; } - - /// - public string Name { get; private set; } - /// - public int AFKTimeout { get; private set; } - /// - public bool IsEmbeddable { get; private set; } - /// - public int VerificationLevel { get; private set; } - - /// - public VoiceRegion VoiceRegion { get; private set; } - /// - public IReadOnlyList Emojis { get; private set; } - /// - public IReadOnlyList Features { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - - /// - public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - /// - public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); - - /// Gets the number of channels in this guild. - public int ChannelCount => _channels.Count; - /// Gets the number of roles in this guild. - public int RoleCount => _roles.Count; - /// Gets the number of users in this guild. - public int UserCount => _userCount; - /// Gets the number of users downloaded for this guild so far. - internal int CurrentUserCount => _members.Count; - - /// Gets the the role representing all users in a guild. - public Role EveryoneRole => GetRole(Id); - public GuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); - /// Gets the user that created this guild. - public GuildUser Owner => GetUser(_ownerId); - /// Gets the default channel for this guild. - public TextChannel DefaultChannel => GetChannel(Id) as TextChannel; - /// Gets the AFK voice channel for this guild. - public VoiceChannel AFKChannel => GetChannel(_afkChannelId.GetValueOrDefault(0)) as VoiceChannel; - /// Gets the embed channel for this guild. - public IChannel EmbedChannel => GetChannel(_embedChannelId.GetValueOrDefault(0)); //TODO: Is this text or voice? - /// Gets a collection of all channels in this guild. - public IEnumerable Channels => _channels.Select(x => Discord.GetChannel(x) as GuildChannel); - /// Gets a collection of text channels in this guild. - public IEnumerable TextChannels => _channels.Select(x => Discord.GetChannel(x) as TextChannel); - /// Gets a collection of voice channels in this guild. - public IEnumerable VoiceChannels => _channels.Select(x => Discord.GetChannel(x) as VoiceChannel); - /// Gets a collection of all roles in this guild. - public IEnumerable Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty(); - /// Gets a collection of all users in this guild. - public IEnumerable Users => _members.Select(x => x.Value); - - internal Guild(DiscordClient discord, Model model) - { - Id = model.Id; - Discord = discord; - - Update(model); - } - private async void Update(Model model) - { - _afkChannelId = model.AFKChannelId; - AFKTimeout = model.AFKTimeout; - _embedChannelId = model.EmbedChannelId; - IsEmbeddable = model.EmbedEnabled; - Features = model.Features; - _iconId = model.Icon; - Name = model.Name; - _ownerId = model.OwnerId; - VoiceRegion = Discord.GetVoiceRegion(model.Region); - _splashId = model.Splash; - VerificationLevel = model.VerificationLevel; - - if (model.Emojis != null) - { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); - for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(new Emoji(model.Emojis[i])); - Emojis = emojis.ToArray(); - } - else - Emojis = Array.Empty(); - - var roles = new ConcurrentDictionary(1, model.Roles?.Length ?? 0); - if (model.Roles != null) - { - for (int i = 0; i < model.Roles.Length; i++) - roles[model.Roles[i].Id] = new Role(this, model.Roles[i]); - } - _roles = roles; - } - - /// - public async Task Modify(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildParams(); - func(args); - await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); - } - /// - public async Task ModifyEmbed(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildEmbedParams(); - func(args); - await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); - } - /// - public async Task ModifyChannels(IEnumerable args) - { - await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); - } - /// - public async Task ModifyRoles(IEnumerable args) - { - await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); - } - /// - public async Task Leave() - { - await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); - } - /// - public async Task Delete() - { - await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); - } - - /// - public async Task> GetBans() - { - var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); - return models.Select(x => new User(Discord, x)); - } - /// - public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); - /// - public async Task AddBan(ulong userId, int pruneDays = 0) - { - var args = new CreateGuildBanParams() { PruneDays = pruneDays }; - await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); - } - /// - public Task RemoveBan(IUser user) => RemoveBan(user.Id); - /// - public async Task RemoveBan(ulong userId) - { - await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); - } - - /// Gets the channel in this guild with the provided id, or null if not found. - public GuildChannel GetChannel(ulong id) - { - if (_channels.ContainsKey(id)) - return Discord.GetChannel(id) as GuildChannel; - return null; - } - /// Creates a new text channel. - public async Task CreateTextChannel(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); - return new TextChannel(this, model); - } - /// Creates a new voice channel. - public async Task CreateVoiceChannel(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); - return new VoiceChannel(this, model); - } - - /// Creates a new integration for this guild. - public async Task CreateIntegration(ulong id, string type) - { - var args = new CreateGuildIntegrationParams { Id = id, Type = type }; - var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); - return new GuildIntegration(this, model); - } - - /// Gets a collection of all invites to this guild. - public async Task> GetInvites() - { - var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); - return models.Select(x => new InviteMetadata(Discord, x)); - } - /// Creates a new invite to this guild. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) - { - if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); - if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); - - var args = new CreateChannelInviteParams() - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - Temporary = isTemporary, - XkcdPass = withXkcd - }; - var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); - return new InviteMetadata(Discord, model); - } - - /// Gets the role in this guild with the provided id, or null if not found. - public Role GetRole(ulong id) - { - Role result = null; - if (_roles?.TryGetValue(id, out result) == true) - return result; - return null; - } - - /// Creates a new role. - public async Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var model = await Discord.ApiClient.CreateGuildRole(Id).ConfigureAwait(false); - var role = new Role(this, model); - - await role.Modify(x => - { - x.Name = name; - x.Permissions = (permissions ?? role.Permissions).RawValue; - x.Color = (color ?? Color.Default).RawValue; - x.Hoist = isHoisted; - }).ConfigureAwait(false); - - return role; - } - - /// Gets the user in this guild with the provided id, or null if not found. - public GuildUser GetUser(ulong id) - { - GuildUser user; - if (_members.TryGetValue(id, out user)) - return user; - return null; - } - public async Task PruneUsers(int days = 30, bool simulate = false) - { - var args = new GuildPruneParams() { Days = days }; - GetGuildPruneCountResponse model; - if (simulate) - model = await Discord.ApiClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); - else - model = await Discord.ApiClient.BeginGuildPrune(Id, args).ConfigureAwait(false); - return model.Pruned; - } - - internal GuildChannel ToChannel(API.Channel model) - { - switch (model.Type) - { - case ChannelType.Text: - default: - return new TextChannel(this, model); - case ChannelType.Voice: - return new VoiceChannel(this, model); - } - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - - IEnumerable IGuild.Emojis => Emojis; - IEnumerable IGuild.Features => Features; - ulong? IGuild.AFKChannelId => _afkChannelId; - ulong IGuild.DefaultChannelId => Id; - ulong? IGuild.EmbedChannelId => _embedChannelId; - ulong IGuild.EveryoneRoleId => EveryoneRole.Id; - ulong IGuild.OwnerId => _ownerId; - string IGuild.VoiceRegionId => VoiceRegion.Id; - bool IUserGuild.IsOwner => CurrentUser.Id == _ownerId; - GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; - - async Task> IGuild.GetBans() - => await GetBans().ConfigureAwait(false); - Task IGuild.GetChannel(ulong id) - => Task.FromResult(GetChannel(id)); - Task> IGuild.GetChannels() - => Task.FromResult>(Channels); - async Task IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) - => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); - async Task IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted) - => await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false); - async Task IGuild.CreateTextChannel(string name) - => await CreateTextChannel(name).ConfigureAwait(false); - async Task IGuild.CreateVoiceChannel(string name) - => await CreateVoiceChannel(name).ConfigureAwait(false); - async Task> IGuild.GetInvites() - => await GetInvites().ConfigureAwait(false); - Task IGuild.GetRole(ulong id) - => Task.FromResult(GetRole(id)); - Task> IGuild.GetRoles() - => Task.FromResult>(Roles); - Task IGuild.GetUser(ulong id) - => Task.FromResult(GetUser(id)); - Task IGuild.GetCurrentUser() - => Task.FromResult(CurrentUser); - Task> IGuild.GetUsers() - => Task.FromResult>(Users); - Task IUpdateable.Update() - => Task.CompletedTask; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs deleted file mode 100644 index 61610a0ae..000000000 --- a/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Integration; - -namespace Discord.WebSocket -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class GuildIntegration : IGuildIntegration - { - /// - public ulong Id { get; private set; } - /// - public string Name { get; private set; } - /// - public string Type { get; private set; } - /// - public bool IsEnabled { get; private set; } - /// - public bool IsSyncing { get; private set; } - /// - public ulong ExpireBehavior { get; private set; } - /// - public ulong ExpireGracePeriod { get; private set; } - /// - public DateTime SyncedAt { get; private set; } - - /// - public Guild Guild { get; private set; } - /// - public Role Role { get; private set; } - /// - public GuildUser User { get; private set; } - /// - public IntegrationAccount Account { get; private set; } - internal DiscordClient Discord => Guild.Discord; - - internal GuildIntegration(Guild guild, Model model) - { - Guild = guild; - Update(model); - } - - private void Update(Model model) - { - Id = model.Id; - Name = model.Name; - Type = model.Type; - IsEnabled = model.Enabled; - IsSyncing = model.Syncing; - ExpireBehavior = model.ExpireBehavior; - ExpireGracePeriod = model.ExpireGracePeriod; - SyncedAt = model.SyncedAt; - - Role = Guild.GetRole(model.RoleId); - User = Guild.GetUser(model.User.Id); - } - - /// - public async Task Delete() - { - await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); - } - /// - public async Task Modify(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildIntegrationParams(); - func(args); - await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); - } - /// - public async Task Sync() - { - await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; - - IGuild IGuildIntegration.Guild => Guild; - IRole IGuildIntegration.Role => Role; - IUser IGuildIntegration.User => User; - IntegrationAccount IGuildIntegration.Account => Account; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Message.cs b/src/Discord.Net/WebSocket/Entities/Message.cs deleted file mode 100644 index af42298f8..000000000 --- a/src/Discord.Net/WebSocket/Entities/Message.cs +++ /dev/null @@ -1,155 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Message; - -namespace Discord.WebSocket -{ - //TODO: Support mention_roles - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Message : IMessage - { - /// - public ulong Id { get; } - - /// - public DateTime? EditedTimestamp { get; private set; } - /// - public bool IsTTS { get; private set; } - /// - public string RawText { get; private set; } - /// - public string Text { get; private set; } - /// - public DateTime Timestamp { get; private set; } - - /// - public IMessageChannel Channel { get; } - /// - public IUser Author { get; } - - /// - public IReadOnlyList Attachments { get; private set; } - /// - public IReadOnlyList Embeds { get; private set; } - /// - public IReadOnlyList MentionedUsers { get; private set; } - /// - public IReadOnlyList MentionedChannels { get; private set; } - /// - public IReadOnlyList MentionedRoles { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; - - internal Message(IMessageChannel channel, IUser author, Model model) - { - Id = model.Id; - Channel = channel; - Author = author; - - Update(model); - } - private void Update(Model model) - { - var guildChannel = Channel as GuildChannel; - var guild = guildChannel?.Guild; - - IsTTS = model.IsTextToSpeech; - Timestamp = model.Timestamp; - EditedTimestamp = model.EditedTimestamp; - RawText = model.Content; - - if (model.Attachments.Length > 0) - { - var attachments = new Attachment[model.Attachments.Length]; - for (int i = 0; i < attachments.Length; i++) - attachments[i] = new Attachment(model.Attachments[i]); - Attachments = ImmutableArray.Create(attachments); - } - else - Attachments = Array.Empty(); - - if (model.Embeds.Length > 0) - { - var embeds = new Embed[model.Attachments.Length]; - for (int i = 0; i < embeds.Length; i++) - embeds[i] = new Embed(model.Embeds[i]); - Embeds = ImmutableArray.Create(embeds); - } - else - Embeds = Array.Empty(); - - if (guildChannel != null && model.Mentions.Length > 0) - { - var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); - for (int i = 0; i < model.Mentions.Length; i++) - { - var user = guild.GetUser(model.Mentions[i].Id); - if (user != null) - builder.Add(user); - } - MentionedUsers = builder.ToArray(); - } - else - MentionedUsers = Array.Empty(); - - if (guildChannel != null/* && model.Content != null*/) - { - MentionedChannels = MentionUtils.GetChannelMentions(model.Content).Select(x => guild.GetChannel(x)).Where(x => x != null).ToImmutableArray(); - - var mentionedRoles = MentionUtils.GetRoleMentions(model.Content).Select(x => guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); - if (model.IsMentioningEveryone) - mentionedRoles = mentionedRoles.Add(guild.EveryoneRole); - MentionedRoles = mentionedRoles; - } - else - { - MentionedChannels = Array.Empty(); - MentionedRoles = Array.Empty(); - } - - Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); - - //Author.Update(model.Author); //TODO: Uncomment this somehow - } - - /// - public async Task Modify(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyMessageParams(); - func(args); - var guildChannel = Channel as GuildChannel; - - if (guildChannel != null) - await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); - else - await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); - } - - /// - public async Task Delete() - { - var guildChannel = Channel as GuildChannel; - if (guildChannel != null) - await Discord.ApiClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); - else - await Discord.ApiClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); - } - - public override string ToString() => Text; - private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; - - IUser IMessage.Author => Author; - IReadOnlyList IMessage.MentionedUsers => MentionedUsers; - IReadOnlyList IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); - IReadOnlyList IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Role.cs b/src/Discord.Net/WebSocket/Entities/Role.cs deleted file mode 100644 index 52bef2b1e..000000000 --- a/src/Discord.Net/WebSocket/Entities/Role.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.Role; - -namespace Discord.WebSocket -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class Role : IRole, IMentionable - { - /// - public ulong Id { get; } - /// Returns the guild this role belongs to. - public Guild Guild { get; } - - /// - public Color Color { get; private set; } - /// - public bool IsHoisted { get; private set; } - /// - public bool IsManaged { get; private set; } - /// - public string Name { get; private set; } - /// - public GuildPermissions Permissions { get; private set; } - /// - public int Position { get; private set; } - - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// - public bool IsEveryone => Id == Guild.Id; - /// - public string Mention => MentionUtils.Mention(this); - public IEnumerable Users => Guild.Users.Where(x => x.Roles.Any(y => y.Id == Id)); - internal DiscordClient Discord => Guild.Discord; - - internal Role(Guild guild, Model model) - { - Id = model.Id; - Guild = guild; - - Update(model); - } - internal void Update(Model model) - { - Name = model.Name; - IsHoisted = model.Hoist.Value; - IsManaged = model.Managed.Value; - Position = model.Position.Value; - Color = new Color(model.Color.Value); - Permissions = new GuildPermissions(model.Permissions.Value); - } - /// Modifies the properties of this role. - public async Task Modify(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildRoleParams(); - func(args); - await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); - } - /// Deletes this message. - public async Task Delete() - => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); - - /// - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - - ulong IRole.GuildId => Guild.Id; - - Task> IRole.GetUsers() - => Task.FromResult>(Users); - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs b/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs deleted file mode 100644 index c0caec225..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Model = Discord.API.GuildMember; - -namespace Discord.WebSocket -{ - public class GuildUser : IGuildUser - { - private ImmutableArray _roles; - - public Guild Guild { get; } - public User GlobalUser { get; } - - /// - public bool IsDeaf { get; private set; } - /// - public bool IsMute { get; private set; } - /// - public DateTime JoinedAt { get; private set; } - /// - public string Nickname { get; private set; } - /// - public UserStatus Status { get; private set; } - /// - public Game? CurrentGame { get; private set; } - /// - public VoiceChannel VoiceChannel { get; private set; } - /// - public GuildPermissions GuildPermissions { get; private set; } - - /// - public IReadOnlyList Roles => _roles; - /// - public string AvatarUrl => GlobalUser.AvatarUrl; - /// - public ushort Discriminator => GlobalUser.Discriminator; - /// - public bool IsBot => GlobalUser.IsBot; - /// - public string Username => GlobalUser.Username; - /// - public DateTime CreatedAt => GlobalUser.CreatedAt; - /// - public ulong Id => GlobalUser.Id; - /// - public string Mention => GlobalUser.Mention; - internal DiscordClient Discord => Guild.Discord; - - internal GuildUser(User globalUser, Guild guild, Model model) - { - GlobalUser = globalUser; - Guild = guild; - - globalUser.Update(model.User); - Update(model); - } - internal void Update(Model model) - { - IsDeaf = model.Deaf; - IsMute = model.Mute; - JoinedAt = model.JoinedAt.Value; - Nickname = model.Nick; - - var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles.Add(Guild.EveryoneRole); - for (int i = 0; i < model.Roles.Length; i++) - roles.Add(Guild.GetRole(model.Roles[i])); - _roles = roles.ToImmutable(); - - UpdateGuildPermissions(); - } - internal void UpdateGuildPermissions() - { - GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); - } - - public async Task Modify(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyGuildMemberParams(); - func(args); - - bool isCurrentUser = Discord.CurrentUser.Id == Id; - if (isCurrentUser && args.Nickname.IsSpecified) - { - var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value }; - await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); - args.Nickname = new API.Optional(); //Remove - } - - if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) - { - await Discord.ApiClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); - if (args.Deaf.IsSpecified) - IsDeaf = args.Deaf.Value; - if (args.Mute.IsSpecified) - IsMute = args.Mute.Value; - if (args.Nickname.IsSpecified) - Nickname = args.Nickname.Value; - if (args.Roles.IsSpecified) - _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); - } - } - - public async Task Kick() - { - await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); - } - - public GuildPermissions GetGuildPermissions() - { - return new GuildPermissions(Permissions.ResolveGuild(this)); - } - public ChannelPermissions GetPermissions(IGuildChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); - } - - public async Task CreateDMChannel() - { - return await GlobalUser.CreateDMChannel().ConfigureAwait(false); - } - - - IGuild IGuildUser.Guild => Guild; - IReadOnlyList IGuildUser.Roles => Roles; - IVoiceChannel IGuildUser.VoiceChannel => VoiceChannel; - - ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) - => GetPermissions(channel); - async Task IUser.CreateDMChannel() - => await CreateDMChannel().ConfigureAwait(false); - Task IUpdateable.Update() - => Task.CompletedTask; - - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs b/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs deleted file mode 100644 index 8b8a86788..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Threading.Tasks; -using Model = Discord.API.User; - -namespace Discord.WebSocket -{ - public class SelfUser : User, ISelfUser - { - /// - public string Email { get; private set; } - /// - public bool IsVerified { get; private set; } - - internal SelfUser(DiscordClient discord, Model model) - : base(discord, model) - { - } - internal override void Update(Model model) - { - base.Update(model); - - Email = model.Email; - IsVerified = model.IsVerified; - } - - /// - public async Task Modify(Action func) - { - if (func != null) throw new NullReferenceException(nameof(func)); - - var args = new ModifyCurrentUserParams(); - func(args); - await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); - } - - Task IUpdateable.Update() - => Task.CompletedTask; - } -} diff --git a/src/Discord.Net/WebSocket/Entities/Users/User.cs b/src/Discord.Net/WebSocket/Entities/Users/User.cs deleted file mode 100644 index e507b4df8..000000000 --- a/src/Discord.Net/WebSocket/Entities/Users/User.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Discord.API.Rest; -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.User; - -namespace Discord.WebSocket -{ - //TODO: Unload when there are no more references via DMUser or GuildUser - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class User : IUser - { - private string _avatarId; - - /// - public ulong Id { get; } - internal DiscordClient Discord { get; } - - /// - public ushort Discriminator { get; private set; } - /// - public bool IsBot { get; private set; } - /// - public string Username { get; private set; } - /// - public DMChannel DMChannel { get; internal set; } - /// - public Game? CurrentGame { get; internal set; } - /// - public UserStatus Status { get; internal set; } - - /// - public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - /// - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - /// - public string Mention => MentionUtils.Mention(this, false); - /// - public string NicknameMention => MentionUtils.Mention(this, true); - - internal User(DiscordClient discord, Model model) - { - Discord = discord; - Id = model.Id; - - Update(model); - } - internal virtual void Update(Model model) - { - _avatarId = model.Avatar; - Discriminator = model.Discriminator; - IsBot = model.Bot; - Username = model.Username; - } - - public async Task CreateDMChannel() - { - var channel = DMChannel; - if (channel == null) - { - var args = new CreateDMChannelParams { RecipientId = Id }; - var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); - - channel = new DMChannel(Discord, this, model); - } - return channel; - } - - public override string ToString() => $"{Username}#{Discriminator}"; - private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; - - /// - async Task IUser.CreateDMChannel() - => await CreateDMChannel().ConfigureAwait(false); - } -} diff --git a/src/Discord.Net/WebSocket/PermissionsCache.cs b/src/Discord.Net/WebSocket/PermissionsCache.cs deleted file mode 100644 index 30f3a0b6e..000000000 --- a/src/Discord.Net/WebSocket/PermissionsCache.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.WebSocket -{ - internal struct ChannelMember - { - public GuildUser User { get; } - public ChannelPermissions Permissions { get; } - - public ChannelMember(GuildUser user, ChannelPermissions permissions) - { - User = user; - Permissions = permissions; - } - } - - internal class PermissionsCache - { - private readonly GuildChannel _channel; - private readonly ConcurrentDictionary _users; - - public IEnumerable Members => _users.Select(x => x.Value); - - public PermissionsCache(GuildChannel channel) - { - _channel = channel; - _users = new ConcurrentDictionary(1, (int)(_channel.Guild.UserCount * 1.05)); - } - - public ChannelMember? Get(ulong id) - { - ChannelMember member; - if (_users.TryGetValue(id, out member)) - return member; - return null; - } - public void Add(GuildUser user) - { - _users[user.Id] = new ChannelMember(user, new ChannelPermissions(Permissions.ResolveChannel(user, _channel, user.GuildPermissions.RawValue))); - } - public void Remove(GuildUser user) - { - ChannelMember member; - _users.TryRemove(user.Id, out member); - } - - public void UpdateAll() - { - foreach (var pair in _users) - { - var member = pair.Value; - var newPerms = Permissions.ResolveChannel(member.User, _channel, member.User.GuildPermissions.RawValue); - if (newPerms != member.Permissions.RawValue) - _users[pair.Key] = new ChannelMember(member.User, new ChannelPermissions(newPerms)); - } - } - public void Update(GuildUser user) - { - ChannelMember member; - if (_users.TryGetValue(user.Id, out member)) - { - var newPerms = Permissions.ResolveChannel(user, _channel, user.GuildPermissions.RawValue); - if (newPerms != member.Permissions.RawValue) - _users[user.Id] = new ChannelMember(user, new ChannelPermissions(newPerms)); - } - } - } -} From 559b89933f6f688cecb7208f0cc8bbd112c04fe6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:19:26 -0300 Subject: [PATCH 002/160] Fixed error when passing no config to DiscordSocketClient --- src/Discord.Net/DiscordClient.cs | 13 +++++++------ src/Discord.Net/DiscordSocketClient.cs | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 0b0d04390..4738ca174 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -3,6 +3,8 @@ using Discord.Extensions; using Discord.Logging; using Discord.Net; using Discord.Net.Queue; +using Discord.Net.Rest; +using Discord.Net.WebSockets; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -28,11 +30,10 @@ namespace Discord public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } - public DiscordClient(DiscordConfig config = null) + public DiscordClient() + : this(new DiscordConfig()) { } + public DiscordClient(DiscordConfig config) { - if (config == null) - config = new DiscordConfig(); - _log = new LogManager(config.LogLevel); _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); @@ -41,10 +42,10 @@ namespace Discord _connectionLock = new SemaphoreSlim(1, 1); _requestQueue = new RequestQueue(); - ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); + ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } - + public async Task Login(TokenType tokenType, string token, bool validateToken = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index af4551872..23a91e915 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -73,11 +73,11 @@ namespace Discord } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); - public DiscordSocketClient(DiscordSocketConfig config = null) + public DiscordSocketClient() + : this(new DiscordSocketConfig()) { } + public DiscordSocketClient(DiscordSocketConfig config) + : base(config) { - if (config == null) - config = new DiscordSocketConfig(); - ShardId = config.ShardId; _totalShards = config.TotalShards; From 1f5f4439272a449ac16c835cc0bf9d9510a4eba8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:19:49 -0300 Subject: [PATCH 003/160] Fixed sending requests with no options obj --- src/Discord.Net/Net/Queue/RestRequest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs index a37204f54..778a03be3 100644 --- a/src/Discord.Net/Net/Queue/RestRequest.cs +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -35,13 +35,15 @@ namespace Discord.Net.Queue private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly, RequestOptions options) { + var timeout = options?.Timeout; + Client = client; Method = method; Endpoint = endpoint; Json = null; MultipartParams = null; HeaderOnly = headerOnly; - TimeoutTick = options.Timeout.HasValue ? (int?)unchecked(Environment.TickCount + options.Timeout.Value) : null; + TimeoutTick = timeout.HasValue ? (int?)unchecked(Environment.TickCount + timeout.Value) : null; Promise = new TaskCompletionSource(); } From d55e0309a0abb2896a435ef202380a45b9b5777a Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:28:38 -0300 Subject: [PATCH 004/160] Renamed OpCode enums --- src/Discord.Net/API/DiscordAPIClient.cs | 16 +++++++--------- .../{GatewayOpCodes.cs => GatewayOpCode.cs} | 2 +- .../Voice/{VoiceOpCodes.cs => VoiceOpCode.cs} | 4 ++-- 3 files changed, 10 insertions(+), 12 deletions(-) rename src/Discord.Net/API/Gateway/{GatewayOpCodes.cs => GatewayOpCode.cs} (97%) rename src/Discord.Net/API/Voice/{VoiceOpCodes.cs => VoiceOpCode.cs} (91%) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index eaa403fa3..9f8af48e1 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -27,7 +27,7 @@ namespace Discord.API { public event Func SentRequest; public event Func SentGatewayMessage; - public event Func ReceivedGatewayEvent; + public event Func ReceivedGatewayEvent; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; @@ -66,14 +66,14 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) { var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); - await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); }; } @@ -198,8 +198,6 @@ namespace Discord.API var url = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; await _gatewayClient.Connect(url).ConfigureAwait(false); - await SendIdentify().ConfigureAwait(false); - ConnectionState = ConnectionState.Connected; } catch (Exception) @@ -321,13 +319,13 @@ namespace Discord.API return responseStream; } - public Task SendGateway(GatewayOpCodes opCode, object payload, + public Task SendGateway(GatewayOpCode opCode, object payload, GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) => SendGateway(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); - public Task SendGateway(GatewayOpCodes opCode, object payload, + public Task SendGateway(GatewayOpCode opCode, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) => SendGateway(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); - private async Task SendGateway(GatewayOpCodes opCode, object payload, + private async Task SendGateway(GatewayOpCode opCode, object payload, BucketGroup group, int bucketId, ulong guildId, RequestOptions options) { //TODO: Add ETF @@ -363,7 +361,7 @@ namespace Discord.API LargeThreshold = largeThreshold, UseCompression = useCompression }; - await SendGateway(GatewayOpCodes.Identify, msg, options: options).ConfigureAwait(false); + await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } //Channels diff --git a/src/Discord.Net/API/Gateway/GatewayOpCodes.cs b/src/Discord.Net/API/Gateway/GatewayOpCode.cs similarity index 97% rename from src/Discord.Net/API/Gateway/GatewayOpCodes.cs rename to src/Discord.Net/API/Gateway/GatewayOpCode.cs index f4d932d90..ac1a21e1d 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCodes.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCode.cs @@ -1,6 +1,6 @@ namespace Discord.API.Gateway { - public enum GatewayOpCodes : byte + public enum GatewayOpCode : byte { /// C←S - Used to send most events. Dispatch = 0, diff --git a/src/Discord.Net/API/Voice/VoiceOpCodes.cs b/src/Discord.Net/API/Voice/VoiceOpCode.cs similarity index 91% rename from src/Discord.Net/API/Voice/VoiceOpCodes.cs rename to src/Discord.Net/API/Voice/VoiceOpCode.cs index 73c7eda0c..b1526b463 100644 --- a/src/Discord.Net/API/Voice/VoiceOpCodes.cs +++ b/src/Discord.Net/API/Voice/VoiceOpCode.cs @@ -1,6 +1,6 @@ -namespace Discord.API.Gateway +namespace Discord.API.Voice { - public enum VoiceOpCodes : byte + public enum VoiceOpCode : byte { /// C→S - Used to associate a connection with a token. Identify = 0, From 2b8e32785d4330a4f13e03a18e6e491d3f80f015 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:28:55 -0300 Subject: [PATCH 005/160] Fixed Guild.Features nullref --- src/Discord.Net/Entities/Guilds/Guild.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index dfd0a041e..2e3842bdb 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -54,14 +54,13 @@ namespace Discord EmbedChannelId = model.EmbedChannelId; AFKTimeout = model.AFKTimeout; IsEmbeddable = model.EmbedEnabled; - Features = model.Features.ToImmutableArray(); _iconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; VoiceRegionId = model.Region; _splashId = model.Splash; VerificationLevel = model.VerificationLevel; - + if (model.Emojis != null) { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); @@ -72,6 +71,11 @@ namespace Discord else Emojis = ImmutableArray.Create(); + if (model.Features != null) + Features = model.Features.ToImmutableArray(); + else + Features = ImmutableArray.Create(); + var roles = new ConcurrentDictionary(1, model.Roles?.Length ?? 0); if (model.Roles != null) { From a5f7053f5558829feec40cfa539162632e097f80 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:29:24 -0300 Subject: [PATCH 006/160] Delayed .Connect completion, added Hello support --- src/Discord.Net/API/Gateway/HelloEvent.cs | 10 +++++++++ src/Discord.Net/API/Gateway/ReadyEvent.cs | 9 ++------ src/Discord.Net/DiscordSocketClient.cs | 27 ++++++++++++++++++----- 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/Discord.Net/API/Gateway/HelloEvent.cs diff --git a/src/Discord.Net/API/Gateway/HelloEvent.cs b/src/Discord.Net/API/Gateway/HelloEvent.cs new file mode 100644 index 000000000..ed201a999 --- /dev/null +++ b/src/Discord.Net/API/Gateway/HelloEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class HelloEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ReadyEvent.cs b/src/Discord.Net/API/Gateway/ReadyEvent.cs index 6e1b05e73..ba4f8756f 100644 --- a/src/Discord.Net/API/Gateway/ReadyEvent.cs +++ b/src/Discord.Net/API/Gateway/ReadyEvent.cs @@ -26,17 +26,12 @@ namespace Discord.API.Gateway public ExtendedGuild[] Guilds { get; set; } [JsonProperty("private_channels")] public Channel[] PrivateChannels { get; set; } - [JsonProperty("heartbeat_interval")] - public int HeartbeatInterval { get; set; } [JsonProperty("relationships")] public Relationship[] Relationships { get; set; } //Ignored - [JsonProperty("user_settings")] - public object UserSettings { get; set; } + /*[JsonProperty("user_settings")] [JsonProperty("user_guild_settings")] - public object UserGuildSettings { get; set; } - [JsonProperty("tutorial")] - public object Tutorial { get; set; } + [JsonProperty("tutorial")]*/ } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 23a91e915..db4de03b2 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -46,6 +46,7 @@ namespace Discord private readonly int _totalShards; private ImmutableDictionary _voiceRegions; private string _sessionId; + private TaskCompletionSource _connectTask; public int ShardId { get; } public ConnectionState ConnectionState { get; private set; } @@ -95,7 +96,7 @@ namespace Discord _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}"); + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {(GatewayOpCode)opCode}"); ApiClient.ReceivedGatewayEvent += ProcessMessage; GatewaySocket = config.WebSocketProvider(); @@ -133,8 +134,10 @@ namespace Discord ConnectionState = ConnectionState.Connecting; try { + _connectTask = new TaskCompletionSource(); await ApiClient.Connect().ConfigureAwait(false); + await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } catch (Exception) @@ -184,9 +187,12 @@ namespace Discord internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null) { var guild = new CachedGuild(this, model); - for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(model.Channels[i], dataStore); - DataStore.AddGuild(guild); + if (model.Unavailable != true) + { + for (int i = 0; i < model.Channels.Length; i++) + AddCachedChannel(model.Channels[i], dataStore); + } + (dataStore ?? DataStore).AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; @@ -253,13 +259,20 @@ namespace Discord return user; } - private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload) + private async Task ProcessMessage(GatewayOpCode opCode, string type, JToken payload) { try { switch (opCode) { - case GatewayOpCodes.Dispatch: + case GatewayOpCode.Hello: + { + var data = payload.ToObject(_serializer); + + await ApiClient.SendIdentify().ConfigureAwait(false); + } + break; + case GatewayOpCode.Dispatch: switch (type) { //Global @@ -280,6 +293,8 @@ namespace Discord DataStore = dataStore; await Ready().ConfigureAwait(false); + + _connectTask.TrySetResult(true); //Signal the .Connect() call to complete } break; From a944b1576a4fd8151f5986d6e6cd70c7699d6758 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:37:11 -0300 Subject: [PATCH 007/160] Fixed a few datastore issues --- src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 2c5f02f33..f2b032331 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -62,7 +62,7 @@ namespace Discord if (model.Channels != null) { for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(model.Channels[i], channels, dataStore); + AddCachedChannel(model.Channels[i], channels); } _channels = channels; @@ -85,10 +85,9 @@ namespace Discord public override Task GetChannel(ulong id) => Task.FromResult(GetCachedChannel(id)); public override Task> GetChannels() => Task.FromResult>(Channels); - public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet channels = null, DataStore dataStore = null) + public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet channels = null) { var channel = ToChannel(model); - (dataStore ?? Discord.DataStore).AddChannel(channel); (channels ?? _channels).TryAdd(model.Id); return channel; } @@ -96,10 +95,9 @@ namespace Discord { return Discord.DataStore.GetChannel(id) as ICachedGuildChannel; } - public ICachedGuildChannel RemoveCachedChannel(ulong id, ConcurrentHashSet channels = null, DataStore dataStore = null) + public void RemoveCachedChannel(ulong id, ConcurrentHashSet channels = null) { (channels ?? _channels).TryRemove(id); - return (dataStore ?? Discord.DataStore).RemoveChannel(id) as ICachedGuildChannel; } public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) From 2fd7f59153fd3aa2b130cd1408021769890af42e Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:37:47 -0300 Subject: [PATCH 008/160] Improved internal websocket interfaces --- src/Discord.Net/Entities/WebSocket/ICachedEntity.cs | 2 +- src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs b/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs index fa004844b..48dc26f2e 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedEntity.cs @@ -1,6 +1,6 @@ namespace Discord { - interface ICachedEntity : IEntity + internal interface ICachedEntity : IEntity { DiscordSocketClient Discord { get; } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs index dc13d1eab..290bff64e 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedGuildChannel.cs @@ -2,5 +2,6 @@ { internal interface ICachedGuildChannel : ICachedChannel, IGuildChannel { + new CachedGuild Guild { get; } } } From 2f51500d254ede6c51eefa45b19ff814189a0455 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:37:57 -0300 Subject: [PATCH 009/160] More datastore fixes --- src/Discord.Net/DiscordSocketClient.cs | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index db4de03b2..8023ac050 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -199,14 +199,14 @@ namespace Discord } internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null) { - var guild = DataStore.RemoveGuild(id) as CachedGuild; + var guild = (dataStore ?? DataStore).RemoveGuild(id) as CachedGuild; foreach (var channel in guild.Channels) guild.RemoveCachedChannel(channel.Id); foreach (var user in guild.Members) guild.RemoveCachedUser(user.Id); return guild; } - internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id) as CachedGuild; + internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id); public override Task GetChannel(ulong id) { @@ -214,29 +214,43 @@ namespace Discord } internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) { + ICachedChannel channel; if (model.IsPrivate) { var recipient = AddCachedUser(model.Recipient); - return recipient.SetDMChannel(model); + channel = recipient.SetDMChannel(model); } else { var guild = GetCachedGuild(model.GuildId.Value); - return guild.AddCachedChannel(model); + channel = guild.AddCachedChannel(model); } + (dataStore ?? DataStore).AddChannel(channel); + return channel; } internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null) { + //TODO: C#7 var channel = DataStore.RemoveChannel(id) as ICachedChannel; + + var guildChannel = channel as ICachedGuildChannel; + if (guildChannel != null) + { + guildChannel.Guild.RemoveCachedChannel(guildChannel.Id); + return channel; + } + var dmChannel = channel as CachedDMChannel; if (dmChannel != null) { var recipient = dmChannel.Recipient; recipient.RemoveDMChannel(id); + return channel; } - return channel; + + return null; } - internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id) as ICachedChannel; + internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id); public override Task GetUser(ulong id) { @@ -248,13 +262,13 @@ namespace Discord } internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) { - var user = DataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; + var user = (dataStore ?? DataStore).GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; user.AddRef(); return user; } internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null) { - var user = DataStore.GetUser(id) as CachedPublicUser; + var user = (dataStore ?? DataStore).GetUser(id) as CachedPublicUser; user.RemoveRef(); return user; } From 9c20253a0ce02c6c53894a09e8eb69cff5aef85c Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 7 Jun 2016 21:44:19 -0300 Subject: [PATCH 010/160] datastore, please --- src/Discord.Net/DiscordSocketClient.cs | 30 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 8023ac050..80a427467 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -186,27 +186,30 @@ namespace Discord } internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null) { + dataStore = dataStore ?? DataStore; + var guild = new CachedGuild(this, model); if (model.Unavailable != true) { for (int i = 0; i < model.Channels.Length; i++) AddCachedChannel(model.Channels[i], dataStore); } - (dataStore ?? DataStore).AddGuild(guild); + dataStore.AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; } internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null) { - var guild = (dataStore ?? DataStore).RemoveGuild(id) as CachedGuild; + dataStore = dataStore ?? DataStore; + + var guild = dataStore.RemoveGuild(id) as CachedGuild; foreach (var channel in guild.Channels) guild.RemoveCachedChannel(channel.Id); foreach (var user in guild.Members) guild.RemoveCachedUser(user.Id); return guild; } - internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id); public override Task GetChannel(ulong id) { @@ -214,22 +217,26 @@ namespace Discord } internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) { + dataStore = dataStore ?? DataStore; + ICachedChannel channel; if (model.IsPrivate) { - var recipient = AddCachedUser(model.Recipient); + var recipient = AddCachedUser(model.Recipient, dataStore); channel = recipient.SetDMChannel(model); } else { - var guild = GetCachedGuild(model.GuildId.Value); + var guild = dataStore.GetGuild(model.GuildId.Value); channel = guild.AddCachedChannel(model); } - (dataStore ?? DataStore).AddChannel(channel); + dataStore.AddChannel(channel); return channel; } internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null) { + dataStore = dataStore ?? DataStore; + //TODO: C#7 var channel = DataStore.RemoveChannel(id) as ICachedChannel; @@ -250,7 +257,6 @@ namespace Discord return null; } - internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id); public override Task GetUser(ulong id) { @@ -262,13 +268,17 @@ namespace Discord } internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) { - var user = (dataStore ?? DataStore).GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; + dataStore = dataStore ?? DataStore; + + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; user.AddRef(); return user; } internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null) { - var user = (dataStore ?? DataStore).GetUser(id) as CachedPublicUser; + dataStore = dataStore ?? DataStore; + + var user = dataStore.GetUser(id) as CachedPublicUser; user.RemoveRef(); return user; } @@ -306,7 +316,7 @@ namespace Discord _sessionId = data.SessionId; DataStore = dataStore; - await Ready().ConfigureAwait(false); + await Ready.Raise().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete } From 5820ca6b5393b6356dbc72206eb850defe7b6cea Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 14:45:19 -0300 Subject: [PATCH 011/160] Changed Discriminators to strings --- src/Discord.Net/API/Common/User.cs | 2 +- src/Discord.Net/API/DiscordAPIClient.cs | 5 +++-- src/Discord.Net/DiscordClient.cs | 4 +--- src/Discord.Net/DiscordSocketClient.cs | 4 ++-- src/Discord.Net/Entities/Users/GuildUser.cs | 2 +- src/Discord.Net/Entities/Users/IUser.cs | 2 +- src/Discord.Net/Entities/Users/User.cs | 2 +- src/Discord.Net/IDiscordClient.cs | 2 +- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net/API/Common/User.cs index c8e566711..7e37d2cf8 100644 --- a/src/Discord.Net/API/Common/User.cs +++ b/src/Discord.Net/API/Common/User.cs @@ -9,7 +9,7 @@ namespace Discord.API [JsonProperty("username")] public string Username { get; set; } [JsonProperty("discriminator")] - public ushort Discriminator { get; set; } + public string Discriminator { get; set; } [JsonProperty("avatar")] public string Avatar { get; set; } [JsonProperty("verified")] diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 9f8af48e1..39a6d25f7 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -994,10 +994,11 @@ namespace Discord.API } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetUser(string username, ushort discriminator, RequestOptions options = null) + public async Task GetUser(string username, string discriminator, RequestOptions options = null) { Preconditions.NotNullOrEmpty(username, nameof(username)); - + Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); + try { var models = await QueryUsers($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 4738ca174..719b5eba1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -3,8 +3,6 @@ using Discord.Extensions; using Discord.Logging; using Discord.Net; using Discord.Net.Queue; -using Discord.Net.Rest; -using Discord.Net.WebSockets; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -190,7 +188,7 @@ namespace Discord return new User(this, model); return null; } - public virtual async Task GetUser(string username, ushort discriminator) + public virtual async Task GetUser(string username, string discriminator) { var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); if (model != null) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 80a427467..be94ecb83 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -95,7 +95,7 @@ namespace Discord _gatewayLogger = _log.CreateLogger("Gateway"); _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {(GatewayOpCode)opCode}"); ApiClient.ReceivedGatewayEvent += ProcessMessage; GatewaySocket = config.WebSocketProvider(); @@ -262,7 +262,7 @@ namespace Discord { return Task.FromResult(DataStore.GetUser(id)); } - public override Task GetUser(string username, ushort discriminator) + public override Task GetUser(string username, string discriminator) { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index ec5c5784f..fa070ca43 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -23,7 +23,7 @@ namespace Discord public ulong Id => User.Id; public string AvatarUrl => User.AvatarUrl; public DateTime CreatedAt => User.CreatedAt; - public ushort Discriminator => User.Discriminator; + public string Discriminator => User.Discriminator; public Game? Game => User.Game; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index 8de684b7a..0b1b04332 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -7,7 +7,7 @@ namespace Discord /// Gets the url to this user's avatar. string AvatarUrl { get; } /// Gets the per-username unique id for this user. - ushort Discriminator { get; } + string Discriminator { get; } /// Returns true if this user is a bot account. bool IsBot { get; } /// Gets the username for this user. diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 5ce282ddf..7efe5239f 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -10,7 +10,7 @@ namespace Discord { private string _avatarId; - public ushort Discriminator { get; private set; } + public string Discriminator { get; private set; } public bool IsBot { get; private set; } public string Username { get; private set; } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index eb89c8452..e3049c9c3 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -31,7 +31,7 @@ namespace Discord Task GetInvite(string inviteIdOrXkcd); Task GetUser(ulong id); - Task GetUser(string username, ushort discriminator); + Task GetUser(string username, string discriminator); Task GetCurrentUser(); Task> QueryUsers(string query, int limit); From fa681612bcdf681662311df556eed921fd11f5ad Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 14:50:35 -0300 Subject: [PATCH 012/160] Minor logging edits --- src/Discord.Net/DiscordSocketClient.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index be94ecb83..a723d13d1 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -95,8 +95,8 @@ namespace Discord _gatewayLogger = _log.CreateLogger("Gateway"); _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {(GatewayOpCode)opCode}"); + + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Debug($"Sent {(GatewayOpCode)opCode}"); ApiClient.ReceivedGatewayEvent += ProcessMessage; GatewaySocket = config.WebSocketProvider(); @@ -725,20 +725,23 @@ namespace Discord case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add case "VOICE_SERVER_UPDATE": //TODO: Add case "RESUMED": //TODO: Add - await _gatewayLogger.Debug($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); + await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); return; //Others default: - await _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); + await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); return; } break; + default: + await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; } } catch (Exception ex) { - await _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); return; } await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); From a831ae948466311f3742776c64fde19694642174 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 18:42:57 -0300 Subject: [PATCH 013/160] Added heartbeats, latency, guild events and channel events --- src/Discord.Net/API/DiscordAPIClient.cs | 10 +- src/Discord.Net/API/WebSocketMessage.cs | 2 +- src/Discord.Net/DiscordClient.cs | 20 ++ src/Discord.Net/DiscordSocketClient.cs | 183 +++++++++++------- .../Entities/Channels/DMChannel.cs | 2 +- .../Entities/Channels/GuildChannel.cs | 2 +- .../Entities/Channels/TextChannel.cs | 2 +- .../Entities/Channels/VoiceChannel.cs | 2 +- .../Entities/Guilds/GuildIntegration.cs | 4 +- src/Discord.Net/Entities/Guilds/UserGuild.cs | 2 +- src/Discord.Net/Entities/Invites/Invite.cs | 2 +- .../Entities/Invites/InviteMetadata.cs | 2 +- src/Discord.Net/Entities/Messages/Message.cs | 2 +- .../Entities/Permissions/Permissions.cs | 17 +- src/Discord.Net/Entities/Users/GuildUser.cs | 8 +- .../Entities/WebSocket/CachedDMChannel.cs | 3 +- .../Entities/WebSocket/CachedGuild.cs | 2 + .../Entities/WebSocket/CachedGuildUser.cs | 2 + .../Entities/WebSocket/CachedPublicUser.cs | 2 +- .../Entities/WebSocket/CachedTextChannel.cs | 1 + .../Entities/WebSocket/CachedVoiceChannel.cs | 2 + .../Entities/WebSocket/ICachedChannel.cs | 7 +- src/Discord.Net/Extensions/EventExtensions.cs | 10 + src/Discord.Net/Net/Rest/DefaultRestClient.cs | 21 +- .../Net/WebSockets/DefaultWebsocketClient.cs | 45 +++-- src/Discord.Net/Utilities/MessageCache.cs | 2 +- 26 files changed, 214 insertions(+), 143 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 39a6d25f7..bf94084ad 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -27,7 +27,7 @@ namespace Discord.API { public event Func SentRequest; public event Func SentGatewayMessage; - public event Func ReceivedGatewayEvent; + public event Func ReceivedGatewayEvent; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; @@ -66,14 +66,14 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) { var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); }; } @@ -363,6 +363,10 @@ namespace Discord.API }; await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } + public async Task SendHeartbeat(int lastSeq, RequestOptions options = null) + { + await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannel(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net/API/WebSocketMessage.cs index 19ec2ac41..3c7d11b70 100644 --- a/src/Discord.Net/API/WebSocketMessage.cs +++ b/src/Discord.Net/API/WebSocketMessage.cs @@ -9,7 +9,7 @@ namespace Discord.API [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] - public uint? Sequence { get; set; } + public int? Sequence { get; set; } [JsonProperty("d")] public object Payload { get; set; } } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 719b5eba1..43829cca1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -28,8 +28,10 @@ namespace Discord public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } + /// Creates a new discord client using only the REST API. public DiscordClient() : this(new DiscordConfig()) { } + /// Creates a new discord client using only the REST API. public DiscordClient(DiscordConfig config) { _log = new LogManager(config.LogLevel); @@ -40,10 +42,12 @@ namespace Discord _connectionLock = new SemaphoreSlim(1, 1); _requestQueue = new RequestQueue(); + //TODO: Is there any better way to do this WebSocketProvider access? ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } + /// public async Task Login(TokenType tokenType, string token, bool validateToken = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -89,6 +93,7 @@ namespace Discord } protected virtual Task OnLogin() => Task.CompletedTask; + /// public async Task Logout() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -115,12 +120,14 @@ namespace Discord } protected virtual Task OnLogout() => Task.CompletedTask; + /// public async Task> GetConnections() { var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); return models.Select(x => new Connection(x)).ToImmutableArray(); } + /// public virtual async Task GetChannel(ulong id) { var model = await ApiClient.GetChannel(id).ConfigureAwait(false); @@ -140,12 +147,14 @@ namespace Discord } return null; } + /// public virtual async Task> GetDMChannels() { var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); } + /// public virtual async Task GetInvite(string inviteIdOrXkcd) { var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); @@ -154,6 +163,7 @@ namespace Discord return null; } + /// public virtual async Task GetGuild(ulong id) { var model = await ApiClient.GetGuild(id).ConfigureAwait(false); @@ -161,6 +171,7 @@ namespace Discord return new Guild(this, model); return null; } + /// public virtual async Task GetGuildEmbed(ulong id) { var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); @@ -168,12 +179,14 @@ namespace Discord return new GuildEmbed(model); return null; } + /// public virtual async Task> GetGuilds() { var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); } + /// public virtual async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); @@ -181,6 +194,7 @@ namespace Discord return new Guild(this, model); } + /// public virtual async Task GetUser(ulong id) { var model = await ApiClient.GetUser(id).ConfigureAwait(false); @@ -188,6 +202,7 @@ namespace Discord return new User(this, model); return null; } + /// public virtual async Task GetUser(string username, string discriminator) { var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); @@ -195,6 +210,7 @@ namespace Discord return new User(this, model); return null; } + /// public virtual async Task GetCurrentUser() { var user = _currentUser; @@ -206,17 +222,20 @@ namespace Discord } return user; } + /// public virtual async Task> QueryUsers(string query, int limit) { var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); return models.Select(x => new User(this, x)).ToImmutableArray(); } + /// public virtual async Task> GetVoiceRegions() { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); } + /// public virtual async Task GetVoiceRegion(string id) { var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); @@ -228,6 +247,7 @@ namespace Discord if (!_isDisposed) _isDisposed = true; } + /// public void Dispose() => Dispose(true); ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index a723d13d1..f6143cf1d 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1,5 +1,4 @@ -using Discord.API; -using Discord.API.Gateway; +using Discord.API.Gateway; using Discord.Data; using Discord.Extensions; using Discord.Logging; @@ -11,19 +10,23 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Discord { //TODO: Remove unnecessary `as` casts - //TODO: Add docstrings + //TODO: Add event docstrings + //TODO: Add reconnect logic (+ensure the heartbeat task shuts down) + //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { public event Func Connected, Disconnected; public event Func Ready; //public event Func VoiceConnected, VoiceDisconnected; - /*public event Func ChannelCreated, ChannelDestroyed; + public event Func ChannelCreated, ChannelDestroyed; public event Func ChannelUpdated; public event Func MessageReceived, MessageDeleted; public event Func MessageUpdated; @@ -34,7 +37,8 @@ namespace Discord public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; public event Func UserUpdated; public event Func CurrentUserUpdated; - public event Func UserIsTyping;*/ + public event Func UserIsTyping; + public event Func LatencyUpdated; private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; @@ -44,13 +48,21 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; - private ImmutableDictionary _voiceRegions; private string _sessionId; + private int _lastSeq; + private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; + private CancellationTokenSource _heartbeatCancelToken; + private Task _heartbeatTask; + private long _heartbeatTime; + /// Gets the shard if of this client. public int ShardId { get; } + /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } - public IWebSocketClient GatewaySocket { get; private set; } + /// Gets the estimated round-trip latency to the gateway server. + public int Latency { get; private set; } + internal IWebSocketClient GatewaySocket { get; private set; } internal int MessageCacheSize { get; private set; } //internal bool UsePermissionCache { get; private set; } internal DataStore DataStore { get; private set; } @@ -61,7 +73,7 @@ namespace Discord get { var guilds = DataStore.Guilds; - return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds); + return guilds.ToReadOnlyCollection(guilds); } } internal IReadOnlyCollection DMChannels @@ -69,13 +81,15 @@ namespace Discord get { var users = DataStore.Users; - return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users); + return users.Select(x => x.DMChannel).Where(x => x != null).ToReadOnlyCollection(users); } } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); + /// Creates a new discord client using the REST and WebSocket APIs. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } + /// Creates a new discord client using the REST and WebSocket APIs. public DiscordSocketClient(DiscordSocketConfig config) : base(config) { @@ -117,6 +131,7 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); } + /// public async Task Connect() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -135,6 +150,7 @@ namespace Discord try { _connectTask = new TaskCompletionSource(); + _heartbeatCancelToken = new CancellationTokenSource(); await ApiClient.Connect().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -148,6 +164,7 @@ namespace Discord await Connected.Raise().ConfigureAwait(false); } + /// public async Task Disconnect() { await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -165,13 +182,15 @@ namespace Discord ConnectionState = ConnectionState.Disconnecting; await ApiClient.Disconnect().ConfigureAwait(false); + await _heartbeatTask.ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; await Disconnected.Raise().ConfigureAwait(false); } - + + /// public override Task GetVoiceRegion(string id) { VoiceRegion region; @@ -180,6 +199,7 @@ namespace Discord return Task.FromResult(null); } + /// public override Task GetGuild(ulong id) { return Task.FromResult(DataStore.GetGuild(id)); @@ -192,7 +212,7 @@ namespace Discord if (model.Unavailable != true) { for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(model.Channels[i], dataStore); + AddCachedChannel(guild, model.Channels[i], dataStore); } dataStore.AddGuild(guild); if (model.Large) @@ -203,7 +223,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var guild = dataStore.RemoveGuild(id) as CachedGuild; + var guild = dataStore.RemoveGuild(id); foreach (var channel in guild.Channels) guild.RemoveCachedChannel(channel.Id); foreach (var user in guild.Members) @@ -211,25 +231,25 @@ namespace Discord return guild; } + /// public override Task GetChannel(ulong id) { return Task.FromResult(DataStore.GetChannel(id)); } - internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) + internal ICachedGuildChannel AddCachedChannel(CachedGuild guild, API.Channel model, DataStore dataStore = null) { dataStore = dataStore ?? DataStore; - ICachedChannel channel; - if (model.IsPrivate) - { - var recipient = AddCachedUser(model.Recipient, dataStore); - channel = recipient.SetDMChannel(model); - } - else - { - var guild = dataStore.GetGuild(model.GuildId.Value); - channel = guild.AddCachedChannel(model); - } + var channel = guild.AddCachedChannel(model); + dataStore.AddChannel(channel); + return channel; + } + internal CachedDMChannel AddCachedDMChannel(API.Channel model, DataStore dataStore = null) + { + dataStore = dataStore ?? DataStore; + + var recipient = AddCachedUser(model.Recipient, dataStore); + var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; } @@ -237,8 +257,8 @@ namespace Discord { dataStore = dataStore ?? DataStore; - //TODO: C#7 - var channel = DataStore.RemoveChannel(id) as ICachedChannel; + //TODO: C#7 Typeswitch Candidate + var channel = DataStore.RemoveChannel(id); var guildChannel = channel as ICachedGuildChannel; if (guildChannel != null) @@ -258,10 +278,12 @@ namespace Discord return null; } + /// public override Task GetUser(ulong id) { return Task.FromResult(DataStore.GetUser(id)); } + /// public override Task GetUser(string username, string discriminator) { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); @@ -270,7 +292,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)); user.AddRef(); return user; } @@ -278,22 +300,34 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetUser(id) as CachedPublicUser; + var user = dataStore.GetUser(id); user.RemoveRef(); return user; } - private async Task ProcessMessage(GatewayOpCode opCode, string type, JToken payload) + private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { + if (seq != null) + _lastSeq = seq.Value; try { switch (opCode) { case GatewayOpCode.Hello: { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); await ApiClient.SendIdentify().ConfigureAwait(false); + _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); + } + break; + case GatewayOpCode.HeartbeatAck: + { + var latency = (int)(Environment.TickCount - _heartbeatTime); + await _gatewayLogger.Debug($"Latency: {latency} ms").ConfigureAwait(false); + Latency = latency; + + await LatencyUpdated.Raise(latency).ConfigureAwait(false); } break; case GatewayOpCode.Dispatch: @@ -303,15 +337,15 @@ namespace Discord case "READY": { //TODO: Make downloading large guilds optional - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - _currentUser = new CachedSelfUser(this,data.User); + _currentUser = new CachedSelfUser(this, data.User); for (int i = 0; i < data.Guilds.Length; i++) AddCachedGuild(data.Guilds[i], dataStore); for (int i = 0; i < data.PrivateChannels.Length; i++) - AddCachedChannel(data.PrivateChannels[i], dataStore); + AddCachedDMChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; DataStore = dataStore; @@ -323,9 +357,9 @@ namespace Discord break; //Guilds - /*case "GUILD_CREATE": + case "GUILD_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = new CachedGuild(this, data); DataStore.AddGuild(guild); @@ -342,12 +376,12 @@ namespace Discord break; case "GUILD_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.Id); if (guild != null) { var before = _enablePreUpdateEvents ? guild.Clone() : null; - guild.Update(data); + guild.Update(data, UpdateSource.WebSocket); await GuildUpdated.Raise(before, guild); } else @@ -356,7 +390,7 @@ namespace Discord break; case "GUILD_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.RemoveGuild(data.Id); if (guild != null) { @@ -375,34 +409,34 @@ namespace Discord //Channels case "CHANNEL_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - IChannel channel = null; + ICachedChannel channel = null; if (data.GuildId != null) { - var guild = GetCachedGuild(data.GuildId.Value); + var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) - channel = guild.AddCachedChannel(data.Id, true); + { + channel = guild.AddCachedChannel(data); + DataStore.AddChannel(channel); + } else await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); } else - channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id); + channel = AddCachedDMChannel(data); if (channel != null) - { - channel.Update(data); await ChannelCreated.Raise(channel); - } } break; case "CHANNEL_UPDATE": { - var data = payload.ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id) as Channel; + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id); if (channel != null) { var before = _enablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data); + channel.Update(data, UpdateSource.WebSocket); await ChannelUpdated.Raise(before, channel); } else @@ -411,7 +445,7 @@ namespace Discord break; case "CHANNEL_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = RemoveCachedChannel(data.Id); if (channel != null) await ChannelDestroyed.Raise(channel); @@ -421,9 +455,9 @@ namespace Discord break; //Members - case "GUILD_MEMBER_ADD": + /*case "GUILD_MEMBER_ADD": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -438,7 +472,7 @@ namespace Discord break; case "GUILD_MEMBER_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -458,7 +492,7 @@ namespace Discord break; case "GUILD_MEMBER_REMOVE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId.Value); if (guild != null) { @@ -479,7 +513,7 @@ namespace Discord break; case "GUILD_MEMBERS_CHUNK": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -498,9 +532,9 @@ namespace Discord break; //Roles - case "GUILD_ROLE_CREATE": + /*case "GUILD_ROLE_CREATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -514,7 +548,7 @@ namespace Discord break; case "GUILD_ROLE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) { @@ -534,8 +568,8 @@ namespace Discord break; case "GUILD_ROLE_DELETE": { - var data = payload.ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId) as CachedGuild; + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { var role = guild.RemoveRole(data.RoleId); @@ -552,7 +586,7 @@ namespace Discord //Bans case "GUILD_BAN_ADD": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetCachedGuild(data.GuildId); if (guild != null) await UserBanned.Raise(new User(this, data)); @@ -574,8 +608,7 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - var data = payload.ToObject(_serializer); - + var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId); if (channel != null) { @@ -599,7 +632,7 @@ namespace Discord break; case "MESSAGE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -614,7 +647,7 @@ namespace Discord break; case "MESSAGE_DELETE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -629,7 +662,7 @@ namespace Discord //Statuses case "PRESENCE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); User user; Guild guild; if (data.GuildId == null) @@ -664,7 +697,7 @@ namespace Discord break; case "TYPING_START": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var channel = GetCachedChannel(data.ChannelId); if (channel != null) { @@ -683,7 +716,7 @@ namespace Discord //Voice case "VOICE_STATE_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); var guild = GetGuild(data.GuildId); if (guild != null) { @@ -708,7 +741,7 @@ namespace Discord //Settings case "USER_UPDATE": { - var data = payload.ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); if (data.Id == CurrentUser.Id) { var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; @@ -746,5 +779,17 @@ namespace Discord } await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); } + private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) + { + var state = ConnectionState; + while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + { + //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect + + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + } } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index eaaf3b8e6..ef8c08c19 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -26,7 +26,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 0cdff457a..461f84076 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -30,7 +30,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected virtual void Update(Model model, UpdateSource source) + public virtual void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 17346d3e6..2c824ffa8 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -22,7 +22,7 @@ namespace Discord : base(guild, model) { } - protected override void Update(Model model, UpdateSource source) + public override void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index fd63ada44..8947c9672 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -17,7 +17,7 @@ namespace Discord : base(guild, model) { } - protected override void Update(Model model, UpdateSource source) + public override void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index 52d002f65..5dbdd6d47 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -31,7 +31,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; @@ -43,7 +43,7 @@ namespace Discord ExpireGracePeriod = model.ExpireGracePeriod; SyncedAt = model.SyncedAt; - Role = Guild.GetRole(model.RoleId) as Role; + Role = Guild.GetRole(model.RoleId); User = new User(Discord, model.User); } diff --git a/src/Discord.Net/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Entities/Guilds/UserGuild.cs index 4eb45342d..a34b40d85 100644 --- a/src/Discord.Net/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/UserGuild.cs @@ -23,7 +23,7 @@ namespace Discord Discord = discord; Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs index c521370ed..d21b93331 100644 --- a/src/Discord.Net/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -26,7 +26,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - protected void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index a4edc761f..8f3ad5a64 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -15,7 +15,7 @@ namespace Discord { Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index e551616d2..4d05e409f 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -36,7 +36,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs index 4c920dd07..8a672489b 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -130,27 +130,14 @@ namespace Discord perms = channel.GetPermissionOverwrite(user); if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - -#if CSHARP7 - switch (channel) - { - case ITextChannel _: - if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - case IVoiceChannel _: - if (!GetValue(resolvedPermissions, ChannelPermission.Connect)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - } -#else + + //TODO: C# Typeswitch candidate var textChannel = channel as ITextChannel; var voiceChannel = channel as IVoiceChannel; if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) resolvedPermissions = 0; //No read permission on a text channel removes all other permissions else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions -#endif resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index fa070ca43..7c72de216 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -39,7 +39,7 @@ namespace Discord Update(model, UpdateSource.Creation); } - private void Update(Model model, UpdateSource source) + public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; @@ -49,9 +49,9 @@ namespace Discord Nickname = model.Nick; var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles.Add(Guild.EveryoneRole as Role); + roles.Add(Guild.EveryoneRole); for (int i = 0; i < model.Roles.Length; i++) - roles.Add(Guild.GetRole(model.Roles[i]) as Role); + roles.Add(Guild.GetRole(model.Roles[i])); Roles = roles.ToImmutable(); GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); @@ -89,7 +89,7 @@ namespace Discord if (args.Nickname.IsSpecified) Nickname = args.Nickname.Value ?? ""; if (args.Roles.IsSpecified) - Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray(); + Roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } } public async Task Kick() diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 33411ee17..6a8d4790a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -65,6 +65,7 @@ namespace Discord public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index f2b032331..32b956e39 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -153,6 +153,8 @@ namespace Discord return null; } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; + new internal ICachedGuildChannel ToChannel(ChannelModel model) { switch (model.Type) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index f1bfa9d14..191fbe05f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -12,5 +12,7 @@ namespace Discord : base(guild, user, model) { } + + public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 142cf15f9..04842ab8a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -16,7 +16,7 @@ namespace Discord { } - public CachedDMChannel SetDMChannel(ChannelModel model) + public CachedDMChannel AddDMChannel(ChannelModel model) { lock (this) { diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index be3d59677..96899a022 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -69,5 +69,6 @@ namespace Discord IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index 6d090d9fa..3b9f42bec 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -34,5 +34,7 @@ namespace Discord } public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; + + ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs index 933ce6226..caebf7c10 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedChannel.cs @@ -1,6 +1,11 @@ -namespace Discord +using Model = Discord.API.Channel; + +namespace Discord { internal interface ICachedChannel : IChannel, ICachedEntity { + void Update(Model model, UpdateSource source); + + ICachedChannel Clone(); } } diff --git a/src/Discord.Net/Extensions/EventExtensions.cs b/src/Discord.Net/Extensions/EventExtensions.cs index 65ab6cebe..867d4d41d 100644 --- a/src/Discord.Net/Extensions/EventExtensions.cs +++ b/src/Discord.Net/Extensions/EventExtensions.cs @@ -6,6 +6,7 @@ namespace Discord.Extensions internal static class EventExtensions { //TODO: Optimize these for if there is only 1 subscriber (can we do this?) + //TODO: Could we maintain our own list instead of generating one on every invocation? public static async Task Raise(this Func eventHandler) { var subscriptions = eventHandler?.GetInvocationList(); @@ -42,5 +43,14 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3).ConfigureAwait(false); } } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + } } } diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 088c09f87..f870cf61e 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -92,25 +92,7 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { -#if CSHARP7 - switch (p.Value) - { - case string value: - content.Add(new StringContent(value), p.Key); - break; - case byte[] value: - content.Add(new ByteArrayContent(value), p.Key); - break; - case Stream value: - content.Add(new StreamContent(value), p.Key); - break; - case MultipartFile value: - content.Add(new StreamContent(value.Stream), value.Filename, p.Key); - break; - default: - throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); - } -#else + //TODO: C# Typeswitch candidate var stringValue = p.Value as string; if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } var byteArrayValue = p.Value as byte[]; @@ -125,7 +107,6 @@ namespace Discord.Net.Rest } throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); -#endif } } restRequest.Content = content; diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 03c965bf5..545a92d37 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -19,6 +19,7 @@ namespace Discord.Net.WebSockets public event Func TextMessage; private readonly ClientWebSocket _client; + private readonly SemaphoreSlim _sendLock; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; @@ -30,6 +31,7 @@ namespace Discord.Net.WebSockets _client.Options.Proxy = null; _client.Options.KeepAliveInterval = TimeSpan.Zero; + _sendLock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; @@ -82,28 +84,37 @@ namespace Discord.Net.WebSockets public async Task Send(byte[] data, int index, int count, bool isText) { - //TODO: If connection is temporarily down, retry? - int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); - - for (int i = 0; i < frameCount; i++, index += SendChunkSize) + await _sendLock.WaitAsync(_cancelToken); + try { - bool isLast = i == (frameCount - 1); + //TODO: If connection is temporarily down, retry? + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); - int frameSize; - if (isLast) - frameSize = count - (i * SendChunkSize); - else - frameSize = SendChunkSize; - - try + for (int i = 0; i < frameCount; i++, index += SendChunkSize) { - await _client.SendAsync(new ArraySegment(data, index, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); - } - catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) - { - return; + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; + + try + { + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; + await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + return; + } } } + finally + { + _sendLock.Release(); + } } //TODO: Check this code diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs index 4b1a35d08..991dde11f 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -74,7 +74,7 @@ namespace Discord { CachedMessage msg; if (_messages.TryGetValue(x, out msg)) - return msg as CachedMessage; + return msg; return null; }) .Where(x => x != null) From 6fbf373848cc52ce33d4acd0d4f8ea988f94d37e Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 18:43:36 -0300 Subject: [PATCH 014/160] Clarify latency is in milliseconds --- src/Discord.Net/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index f6143cf1d..fe33f223a 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -60,7 +60,7 @@ namespace Discord public int ShardId { get; } /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } - /// Gets the estimated round-trip latency to the gateway server. + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } internal IWebSocketClient GatewaySocket { get; private set; } internal int MessageCacheSize { get; private set; } From 0c3b02f5a42533dad8e3945498e230837a31e994 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 19:35:39 -0300 Subject: [PATCH 015/160] Added initial work for member downloading --- src/Discord.Net/API/DiscordAPIClient.cs | 4 ++ .../API/Gateway/RequestMembersParams.cs | 3 +- src/Discord.Net/DiscordSocketClient.cs | 69 +++++++++++++++---- src/Discord.Net/Entities/Guilds/Guild.cs | 1 + src/Discord.Net/Entities/Guilds/IGuild.cs | 3 + .../Entities/WebSocket/CachedGuild.cs | 19 +++++ 6 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index bf94084ad..c6007d2dd 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -367,6 +367,10 @@ namespace Discord.API { await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } + public async Task SendRequestMembers(IEnumerable guildIds, RequestOptions options = null) + { + await SendGateway(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannel(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs index ed6edc6ef..f11be49b1 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -1,11 +1,12 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Gateway { public class RequestMembersParams { [JsonProperty("guild_id")] - public ulong[] GuildId { get; set; } + public IEnumerable GuildIds { get; set; } [JsonProperty("query")] public string Query { get; set; } [JsonProperty("limit")] diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index fe33f223a..177a44694 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -17,9 +17,8 @@ using System.Threading.Tasks; namespace Discord { - //TODO: Remove unnecessary `as` casts //TODO: Add event docstrings - //TODO: Add reconnect logic (+ensure the heartbeat task shuts down) + //TODO: Add reconnect logic (+ensure the heartbeat task to shut down) //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { @@ -32,7 +31,7 @@ namespace Discord public event Func MessageUpdated; public event Func RoleCreated, RoleDeleted; public event Func RoleUpdated; - public event Func JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable; + public event Func JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable, GuildDownloadedMembers; public event Func GuildUpdated; public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; public event Func UserUpdated; @@ -305,6 +304,47 @@ namespace Discord return user; } + /// Downloads the members list for all large guilds. + public Task DownloadAllMembers() + => DownloadMembers(DataStore.Guilds.Where(x => !x.HasAllMembers)); + /// Downloads the members list for the provided guilds, if they don't have a complete list. + public async Task DownloadMembers(IEnumerable guilds) + { + const short batchSize = 50; + var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); + if (cachedGuilds.Length == 0) + return; + else if (cachedGuilds.Length == 1) + { + await cachedGuilds[0].DownloadMembers().ConfigureAwait(false); + return; + } + + ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; + Task[] batchTasks = new Task[batchIds.Length]; + int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; + + for (int i = 0, k = 0; i < batchCount; i++) + { + bool isLast = i == batchCount - 1; + int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; + + for (int j = 0; j < count; j++, k++) + { + var guild = cachedGuilds[k]; + batchIds[j] = guild.Id; + batchTasks[j] = guild.DownloaderPromise; + } + + ApiClient.SendRequestMembers(batchIds); + + if (isLast && batchCount > 1) + await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); + else + await Task.WhenAll(batchTasks).ConfigureAwait(false); + } + } + private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { if (seq != null) @@ -367,11 +407,8 @@ namespace Discord type = "GUILD_AVAILABLE"; else await JoinedGuild.Raise(guild).ConfigureAwait(false); - - if (!data.Large) - await GuildAvailable.Raise(guild); - else - _largeGuilds.Enqueue(data.Id); + + await GuildAvailable.Raise(guild); } break; case "GUILD_UPDATE": @@ -781,15 +818,19 @@ namespace Discord } private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) { - var state = ConnectionState; - while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + try { - //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect + var state = ConnectionState; + while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + { + //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect - _heartbeatTime = Environment.TickCount; - await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); - await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } } + catch (OperationCanceledException) { } } } } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 2e3842bdb..65ce5ec71 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -306,6 +306,7 @@ namespace Discord IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; + Task IGuild.DownloadUsers() { throw new NotSupportedException(); } IRole IGuild.GetRole(ulong id) => GetRole(id); } diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 013265fd3..8d86dcd8b 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -90,6 +90,9 @@ namespace Discord Task GetUser(ulong id); /// Gets the current user for this guild. Task GetCurrentUser(); + /// Downloads all users for this guild if the current list is incomplete. + Task DownloadUsers(); + /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsers(int days = 30, bool simulate = false); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 32b956e39..8890c8230 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -16,6 +16,7 @@ namespace Discord { internal class CachedGuild : Guild, ICachedEntity { + private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _presences; @@ -23,6 +24,9 @@ namespace Discord public bool Available { get; private set; } //TODO: Add to IGuild + public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public Task DownloaderPromise => _downloaderPromise.Task; + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); @@ -30,6 +34,7 @@ namespace Discord public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) { + _downloaderPromise = new TaskCompletionSource(); } public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) @@ -79,6 +84,9 @@ namespace Discord { for (int i = 0; i < model.Members.Length; i++) AddCachedUser(model.Members[i], members, dataStore); + _downloaderPromise = new TaskCompletionSource(); + if (!model.Large) + _downloaderPromise.SetResult(true); } _members = members; } @@ -153,6 +161,17 @@ namespace Discord return null; } + public async Task DownloadMembers() + { + if (!HasAllMembers) + await Discord.ApiClient.SendRequestMembers(new ulong[] { Id }).ConfigureAwait(false); + await _downloaderPromise.Task.ConfigureAwait(false); + } + public void CompleteDownloadMembers() + { + _downloaderPromise.SetResult(true); + } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; new internal ICachedGuildChannel ToChannel(ChannelModel model) From c64bdb83b40f3b03bc173bc960ff7f191c39f52d Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 8 Jun 2016 23:29:50 -0300 Subject: [PATCH 016/160] Added support for more events, added benchmark --- src/Discord.Net/API/Common/GuildMember.cs | 4 +- src/Discord.Net/API/Common/Message.cs | 4 +- src/Discord.Net/API/Common/Presence.cs | 2 + src/Discord.Net/API/Common/VoiceState.cs | 4 +- .../API/Gateway/GuildMemberAddEvent.cs | 10 + .../API/Gateway/GuildMemberRemoveEvent.cs | 12 + .../API/Gateway/GuildMemberUpdateEvent.cs | 10 + .../API/Gateway/GuildRoleCreateEvent.cs | 2 +- .../API/Gateway/GuildRoleUpdateEvent.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 832 +++++++++--------- src/Discord.Net/Entities/Roles/Role.cs | 2 + src/Discord.Net/Entities/Users/GuildUser.cs | 24 +- src/Discord.Net/Entities/Users/IGuildUser.cs | 4 +- src/Discord.Net/Entities/Users/IVoiceState.cs | 16 + src/Discord.Net/Entities/Users/SelfUser.cs | 2 +- src/Discord.Net/Entities/Users/User.cs | 2 +- .../Entities/WebSocket/CachedDMChannel.cs | 10 +- .../Entities/WebSocket/CachedGuild.cs | 68 +- .../Entities/WebSocket/CachedGuildUser.cs | 17 +- .../Entities/WebSocket/CachedPublicUser.cs | 18 +- .../Entities/WebSocket/CachedSelfUser.cs | 3 +- .../Entities/WebSocket/CachedTextChannel.cs | 14 +- .../WebSocket/ICachedMessageChannel.cs | 6 +- .../Entities/WebSocket/ICachedUser.cs | 7 + .../Entities/WebSocket/IVoiceState.cs.old | 62 -- .../Entities/WebSocket/VoiceState.cs | 42 + src/Discord.Net/project.json | 1 + 27 files changed, 678 insertions(+), 502 deletions(-) create mode 100644 src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs create mode 100644 src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs create mode 100644 src/Discord.Net/Entities/Users/IVoiceState.cs create mode 100644 src/Discord.Net/Entities/WebSocket/ICachedUser.cs delete mode 100644 src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old create mode 100644 src/Discord.Net/Entities/WebSocket/VoiceState.cs diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index 03da0d5bf..d54f8b3a6 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -14,8 +14,8 @@ namespace Discord.API [JsonProperty("joined_at")] public DateTime?JoinedAt { get; set; } [JsonProperty("deaf")] - public bool Deaf { get; set; } + public bool? Deaf { get; set; } [JsonProperty("mute")] - public bool Mute { get; set; } + public bool? Mute { get; set; } } } diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index ad8fc2bbe..f2ef47be3 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -27,7 +27,7 @@ namespace Discord.API public Attachment[] Attachments { get; set; } [JsonProperty("embeds")] public Embed[] Embeds { get; set; } - [JsonProperty("nonce")] - public uint? Nonce { get; set; } + /*[JsonProperty("nonce")] + public object Nonce { get; set; }*/ } } diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net/API/Common/Presence.cs index 5f2b853e6..ce4edfb0f 100644 --- a/src/Discord.Net/API/Common/Presence.cs +++ b/src/Discord.Net/API/Common/Presence.cs @@ -6,6 +6,8 @@ namespace Discord.API { [JsonProperty("user")] public User User { get; set; } + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } [JsonProperty("game")] diff --git a/src/Discord.Net/API/Common/VoiceState.cs b/src/Discord.Net/API/Common/VoiceState.cs index e848ea919..49a7409d0 100644 --- a/src/Discord.Net/API/Common/VoiceState.cs +++ b/src/Discord.Net/API/Common/VoiceState.cs @@ -4,8 +4,10 @@ namespace Discord.API { public class VoiceState { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } + public ulong? ChannelId { get; set; } [JsonProperty("user_id")] public ulong UserId { get; set; } [JsonProperty("session_id")] diff --git a/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs new file mode 100644 index 000000000..3676439d4 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberAddEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberAddEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs new file mode 100644 index 000000000..2916c5a91 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberRemoveEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberRemoveEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs new file mode 100644 index 000000000..8221b1199 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMemberUpdateEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMemberUpdateEvent : GuildMember + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs index f05543bf6..5753a638b 100644 --- a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs +++ b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs @@ -7,6 +7,6 @@ namespace Discord.API.Gateway [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("role")] - public Role Data { get; set; } + public Role Role { get; set; } } } diff --git a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs index 345154432..9e88b5de8 100644 --- a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs +++ b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs @@ -7,6 +7,6 @@ namespace Discord.API.Gateway [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("role")] - public Role Data { get; set; } + public Role Role { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 177a44694..449817b1c 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -41,6 +41,9 @@ namespace Discord private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; +#if BENCHMARK + private readonly Logger _benchmarkLogger; +#endif private readonly DataStoreProvider _dataStoreProvider; private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; @@ -106,7 +109,10 @@ namespace Discord _largeThreshold = config.LargeThreshold; _gatewayLogger = _log.CreateLogger("Gateway"); - +#if BENCHMARK + _benchmarkLogger = _log.CreateLogger("Benchmark"); +#endif + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Debug($"Sent {(GatewayOpCode)opCode}"); @@ -207,7 +213,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var guild = new CachedGuild(this, model); + var guild = new CachedGuild(this, model, dataStore); if (model.Unavailable != true) { for (int i = 0; i < model.Channels.Length; i++) @@ -247,7 +253,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var recipient = AddCachedUser(model.Recipient, dataStore); + var recipient = GetOrAddCachedUser(model.Recipient, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; @@ -287,7 +293,7 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } - internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) + internal CachedPublicUser GetOrAddCachedUser(API.User model, DataStore dataStore = null) { dataStore = dataStore ?? DataStore; @@ -299,8 +305,7 @@ namespace Discord { dataStore = dataStore ?? DataStore; - var user = dataStore.GetUser(id); - user.RemoveRef(); + var user = dataStore.RemoveUser(id); return user; } @@ -336,7 +341,7 @@ namespace Discord batchTasks[j] = guild.DownloaderPromise; } - ApiClient.SendRequestMembers(batchIds); + await ApiClient.SendRequestMembers(batchIds).ConfigureAwait(false); if (isLast && batchCount > 1) await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); @@ -347,474 +352,511 @@ namespace Discord private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) { - if (seq != null) - _lastSeq = seq.Value; +#if BENCHMARK + Stopwatch stopwatch = Stopwatch.StartNew(); try { - switch (opCode) +#endif + if (seq != null) + _lastSeq = seq.Value; + try { - case GatewayOpCode.Hello: - { - var data = (payload as JToken).ToObject(_serializer); - - await ApiClient.SendIdentify().ConfigureAwait(false); - _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); - } - break; - case GatewayOpCode.HeartbeatAck: - { - var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.Debug($"Latency: {latency} ms").ConfigureAwait(false); - Latency = latency; - - await LatencyUpdated.Raise(latency).ConfigureAwait(false); - } - break; - case GatewayOpCode.Dispatch: - switch (type) - { - //Global - case "READY": - { - //TODO: Make downloading large guilds optional - var data = (payload as JToken).ToObject(_serializer); - var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - - _currentUser = new CachedSelfUser(this, data.User); - - for (int i = 0; i < data.Guilds.Length; i++) - AddCachedGuild(data.Guilds[i], dataStore); - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddCachedDMChannel(data.PrivateChannels[i], dataStore); - - _sessionId = data.SessionId; - DataStore = dataStore; - - await Ready.Raise().ConfigureAwait(false); - - _connectTask.TrySetResult(true); //Signal the .Connect() call to complete - } - break; - - //Guilds - case "GUILD_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = new CachedGuild(this, data); - DataStore.AddGuild(guild); - - if (data.Unavailable == false) - type = "GUILD_AVAILABLE"; - else - await JoinedGuild.Raise(guild).ConfigureAwait(false); - - await GuildAvailable.Raise(guild); - } - break; - case "GUILD_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); - if (guild != null) + switch (opCode) + { + case GatewayOpCode.Hello: + { + await _gatewayLogger.Debug($"Received Hello").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + await ApiClient.SendIdentify().ConfigureAwait(false); + _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); + } + break; + case GatewayOpCode.HeartbeatAck: + { + await _gatewayLogger.Debug($"Received HeartbeatAck").ConfigureAwait(false); + + var latency = (int)(Environment.TickCount - _heartbeatTime); + await _gatewayLogger.Debug($"Latency = {latency} ms").ConfigureAwait(false); + Latency = latency; + + await LatencyUpdated.Raise(latency).ConfigureAwait(false); + } + break; + case GatewayOpCode.Dispatch: + switch (type) + { + //Global + case "READY": { - var before = _enablePreUpdateEvents ? guild.Clone() : null; - guild.Update(data, UpdateSource.WebSocket); - await GuildUpdated.Raise(before, guild); + await _gatewayLogger.Debug($"Received Dispatch (READY)").ConfigureAwait(false); + + //TODO: Make downloading large guilds optional + var data = (payload as JToken).ToObject(_serializer); + var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + + _currentUser = new CachedSelfUser(this, data.User); + + for (int i = 0; i < data.Guilds.Length; i++) + AddCachedGuild(data.Guilds[i], dataStore); + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddCachedDMChannel(data.PrivateChannels[i], dataStore); + + _sessionId = data.SessionId; + DataStore = dataStore; + + await Ready.Raise().ConfigureAwait(false); + + _connectTask.TrySetResult(true); //Signal the .Connect() call to complete } - else - await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.RemoveGuild(data.Id); - if (guild != null) + break; + + //Guilds + case "GUILD_CREATE": { - if (data.Unavailable == true) - type = "GUILD_UNAVAILABLE"; + var data = (payload as JToken).ToObject(_serializer); + var guild = new CachedGuild(this, data, DataStore); + DataStore.AddGuild(guild); + + if (data.Unavailable == false) + type = "GUILD_AVAILABLE"; + await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); - await GuildUnavailable.Raise(guild); - if (data.Unavailable != true) - await LeftGuild.Raise(guild); + if (data.Unavailable != false) + await JoinedGuild.Raise(guild).ConfigureAwait(false); + + await GuildAvailable.Raise(guild).ConfigureAwait(false); } - else - await _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild."); - } - break; - - //Channels - case "CHANNEL_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - - ICachedChannel channel = null; - if (data.GuildId != null) + break; + case "GUILD_UPDATE": { - var guild = DataStore.GetGuild(data.GuildId.Value); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.Id); if (guild != null) { - channel = guild.AddCachedChannel(data); - DataStore.AddChannel(channel); + var before = _enablePreUpdateEvents ? guild.Clone() : null; + guild.Update(data, UpdateSource.WebSocket); + await GuildUpdated.Raise(before, guild).ConfigureAwait(false); } else - await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); } - else - channel = AddCachedDMChannel(data); - if (channel != null) - await ChannelCreated.Raise(channel); - } - break; - case "CHANNEL_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id); - if (channel != null) + break; + case "GUILD_DELETE": { - var before = _enablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.Raise(before, channel); + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); + + var guild = DataStore.RemoveGuild(data.Id); + if (guild != null) + { + await GuildUnavailable.Raise(guild).ConfigureAwait(false); + if (data.Unavailable != true) + await LeftGuild.Raise(guild).ConfigureAwait(false); + foreach (var member in guild.Members) + member.User.RemoveRef(); + } + else + await _gatewayLogger.Warning($"{type} referenced an unknown guild.").ConfigureAwait(false); } - else - await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); - } - break; - case "CHANNEL_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = RemoveCachedChannel(data.Id); - if (channel != null) - await ChannelDestroyed.Raise(channel); - else - await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); - } - break; - - //Members - /*case "GUILD_MEMBER_ADD": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + + //Channels + case "CHANNEL_CREATE": { - var user = guild.AddCachedUser(data.User.Id, true, true); - user.Update(data); - user.UpdateActivity(); - UserJoined.Raise(user); + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + ICachedChannel channel = null; + if (data.GuildId != null) + { + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) + { + channel = guild.AddCachedChannel(data); + DataStore.AddChannel(channel); + } + else + await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + } + else + channel = AddCachedDMChannel(data); + if (channel != null) + await ChannelCreated.Raise(channel); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + case "CHANNEL_UPDATE": { - var user = guild.GetCachedUser(data.User.Id); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id); + if (channel != null) { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - await UserUpdated.Raise(before, user); + var before = _enablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data, UpdateSource.WebSocket); + await ChannelUpdated.Raise(before, channel); } else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); + } + break; + case "CHANNEL_DELETE": + { + await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = RemoveCachedChannel(data.Id); + if (channel != null) + await ChannelDestroyed.Raise(channel); + else + await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_REMOVE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId.Value); - if (guild != null) + break; + + //Members + case "GUILD_MEMBER_ADD": { - var user = guild.RemoveCachedUser(data.User.Id); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - user.GlobalUser.RemoveGuild(); - if (user.GuildCount == 0 && user.DMChannel == null) - DataStore.RemoveUser(user.Id); - await UserLeft.Raise(user); + var user = guild.AddCachedUser(data); + await UserJoined.Raise(user).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); - } - break; - case "GUILD_MEMBERS_CHUNK": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBER_UPDATE": { - foreach (var memberData in data.Members) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - var user = guild.AddCachedUser(memberData.User.Id, true, false); - user.Update(memberData); + var user = guild.GetCachedUser(data.User.Id); + if (user != null) + { + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data, UpdateSource.WebSocket); + await UserUpdated.Raise(before, user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); } - - if (guild.CurrentUserCount >= guild.UserCount) //Finished downloading for there - await GuildAvailable.Raise(guild); + else + await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); - } - break; - - //Roles - /*case "GUILD_ROLE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBER_REMOVE": { - var role = guild.AddCachedRole(data.Data.Id); - role.Update(data.Data, false); - RoleCreated.Raise(role); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.RemoveCachedUser(data.User.Id); + if (user != null) + { + user.User.RemoveRef(); + await UserLeft.Raise(user); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + } + else + await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) + break; + case "GUILD_MEMBERS_CHUNK": { - var role = guild.GetRole(data.Data.Id); - if (role != null) + await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - var before = _enablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Data, true); - RoleUpdated.Raise(before, role); + foreach (var memberModel in data.Members) + guild.AddCachedUser(memberModel); + + if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there + { + guild.CompleteDownloadMembers(); + await GuildDownloadedMembers.Raise(guild).ConfigureAwait(false); + } } else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) + break; + + //Roles + case "GUILD_ROLE_CREATE": { - var role = guild.RemoveRole(data.RoleId); - if (role != null) - RoleDeleted.Raise(role); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddCachedRole(data.Role); + await RoleCreated.Raise(role).ConfigureAwait(false); + } else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); - } - break; - - //Bans - case "GUILD_BAN_ADD": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) - await UserBanned.Raise(new User(this, data)); - else - await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); - } - break; - case "GUILD_BAN_REMOVE": - { - var data = payload.ToObject(_serializer); - var guild = GetCachedGuild(data.GuildId); - if (guild != null) - await UserUnbanned.Raise(new User(this, data)); - else - await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); - } - break; - - //Messages - case "MESSAGE_CREATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId); - if (channel != null) + break; + case "GUILD_ROLE_UPDATE": { - var user = channel.GetUser(data.Author.Id); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); - if (user != null) + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - bool isAuthor = data.Author.Id == CurrentUser.Id; - var msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); - - msg.Update(data); - - MessageReceived.Raise(msg); + var role = guild.GetRole(data.Role.Id); + if (role != null) + { + var before = _enablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Role, UpdateSource.WebSocket); + await RoleUpdated.Raise(before, role).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); } else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); - } - break; - case "MESSAGE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + case "GUILD_ROLE_DELETE": { - var msg = channel.GetMessage(data.Id, data.Author?.Id); - var before = _enablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data); - MessageUpdated.Raise(before, msg); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.RemoveCachedRole(data.RoleId); + if (role != null) + await RoleDeleted.Raise(role).ConfigureAwait(false); + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + } + else + await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); - } - break; - case "MESSAGE_DELETE": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + + //Bans + case "GUILD_BAN_ADD": { - var msg = channel.RemoveMessage(data.Id); - MessageDeleted.Raise(msg); + await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserBanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); } - else - await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); - } - break; - - //Statuses - case "PRESENCE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - User user; - Guild guild; - if (data.GuildId == null) + break; + case "GUILD_BAN_REMOVE": { - guild = null; - user = GetPrivateChannel(data.User.Id)?.Recipient; + await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserUnbanned.Raise(new User(this, data)); + else + await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); } - else + break; + + //Messages + case "MESSAGE_CREATE": { - guild = GetGuild(data.GuildId.Value); - if (guild == null) + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) { - await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); - break; + var author = channel.GetCachedUser(data.Author.Id); + + if (author != null) + { + var msg = channel.AddCachedMessage(author, data); + await MessageReceived.Raise(msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); } else - user = guild.GetUser(data.User.Id); + await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); } - - if (user != null) + break; + case "MESSAGE_UPDATE": { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - UserUpdated.Raise(before, user); + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var msg = channel.GetCachedMessage(data.Id); + var before = _enablePreUpdateEvents ? msg.Clone() : null; + msg.Update(data, UpdateSource.WebSocket); + await MessageUpdated.Raise(before, msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); } - else + break; + case "MESSAGE_DELETE": { - //Occurs when a user leaves a guild - //await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user."); + await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var msg = channel.RemoveCachedMessage(data.Id); + await MessageDeleted.Raise(msg).ConfigureAwait(false); + } + else + await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); } - } - break; - case "TYPING_START": - { - var data = (payload as JToken).ToObject(_serializer); - var channel = GetCachedChannel(data.ChannelId); - if (channel != null) + break; + + //Statuses + case "PRESENCE_UPDATE": { - var user = channel.GetUser(data.UserId); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId == null) + { + var user = DataStore.GetUser(data.User.Id); + if (user == null) + user.Update(data, UpdateSource.WebSocket); + } + else { - await UserIsTyping.Raise(channel, user); - user.UpdateActivity(); + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild == null) + { + await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); + break; + } + if (data.Status == UserStatus.Offline) + guild.RemoveCachedPresence(data.User.Id); + else + guild.AddOrUpdateCachedPresence(data); } } - else - await _gatewayLogger.Warning("TYPING_START referenced an unknown channel."); - } - break; - - //Voice - case "VOICE_STATE_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - var guild = GetGuild(data.GuildId); - if (guild != null) + break; + case "TYPING_START": { - var user = guild.GetUser(data.UserId); - if (user != null) + await _gatewayLogger.Debug($"Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - UserUpdated.Raise(before, user); + var user = channel.GetCachedUser(data.UserId); + if (user != null) + await UserIsTyping.Raise(channel, user).ConfigureAwait(false); } else + await _gatewayLogger.Warning("TYPING_START referenced an unknown channel.").ConfigureAwait(false); + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.Debug($"Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.HasValue) { - //Occurs when a user leaves a guild - //await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) + { + if (data.ChannelId == null) + guild.RemoveCachedVoiceState(data.UserId); + else + guild.AddOrUpdateCachedVoiceState(data); + + var user = guild.GetCachedUser(data.UserId); + user.Update(data, UpdateSource.WebSocket); + } + else + await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); } } - else - await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild."); - } - break; - - //Settings - case "USER_UPDATE": - { - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) + break; + + //Settings + case "USER_UPDATE": { - var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; - CurrentUser.Update(data); - await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + await _gatewayLogger.Debug($"Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data, UpdateSource.WebSocket); + await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + } } - } - 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 - case "RESUMED": //TODO: Add - await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); - return; - - //Others - default: - await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); - return; - } - break; - default: - await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); - return; + 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 + case "RESUMED": //TODO: Add + await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); + return; + + //Others + default: + await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); + return; + } + break; + default: + await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; + } } + catch (Exception ex) + { + await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + return; + } +#if BENCHMARK } - catch (Exception ex) + finally { - await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - return; + stopwatch.Stop(); + double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + await _benchmarkLogger.Debug($"{millis} ms").ConfigureAwait(false); } - await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); +#endif } private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) { diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs index 578930b42..577a4c252 100644 --- a/src/Discord.Net/Entities/Roles/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -57,6 +57,8 @@ namespace Discord { await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); } + + public Role Clone() => MemberwiseClone() as Role; public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 7c72de216..dd879dd20 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; +using VoiceStateModel = Discord.API.VoiceState; namespace Discord { @@ -24,12 +25,12 @@ namespace Discord public string AvatarUrl => User.AvatarUrl; public DateTime CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; - public Game? Game => User.Game; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; - public UserStatus Status => User.Status; public string Username => User.Username; + public virtual UserStatus Status => User.Status; + public virtual Game? Game => User.Game; public DiscordClient Discord => Guild.Discord; @@ -43,8 +44,10 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - IsDeaf = model.Deaf; - IsMute = model.Mute; + if (model.Deaf.HasValue) + IsDeaf = model.Deaf.Value; + if (model.Mute.HasValue) + IsMute = model.Mute.Value; JoinedAt = model.JoinedAt.Value; Nickname = model.Nick; @@ -56,6 +59,13 @@ namespace Discord GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } + public void Update(VoiceStateModel model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + IsDeaf = model.Deaf; + IsMute = model.Mute; + } public async Task Update() { @@ -107,6 +117,10 @@ namespace Discord IGuild IGuildUser.Guild => Guild; IReadOnlyCollection IGuildUser.Roles => Roles; - IVoiceChannel IGuildUser.VoiceChannel => null; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 63b7c12fb..5b68af6ca 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -6,7 +6,7 @@ using Discord.API.Rest; namespace Discord { /// A Guild-User pairing. - public interface IGuildUser : IUpdateable, IUser + public interface IGuildUser : IUpdateable, IUser, IVoiceState { /// Returns true if the guild has deafened this user. bool IsDeaf { get; } @@ -23,8 +23,6 @@ namespace Discord IGuild Guild { get; } /// Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. IReadOnlyCollection Roles { get; } - /// Gets the voice channel this user is currently in, if any. - IVoiceChannel VoiceChannel { get; } /// Gets the channel-level permissions granted to this user for a given channel. ChannelPermissions GetPermissions(IGuildChannel channel); diff --git a/src/Discord.Net/Entities/Users/IVoiceState.cs b/src/Discord.Net/Entities/Users/IVoiceState.cs new file mode 100644 index 000000000..8bdd7436c --- /dev/null +++ b/src/Discord.Net/Entities/Users/IVoiceState.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public interface IVoiceState + { + /// Returns true if this user has marked themselves as deafened. + bool IsSelfDeafened { get; } + /// Returns true if this user has marked themselves as muted. + bool IsSelfMuted { get; } + /// Returns true if the guild is temporarily blocking audio to/from this user. + bool IsSuppressed { get; } + /// Gets the voice channel this user is currently in, if any. + IVoiceChannel VoiceChannel { get; } + /// Gets the unique identifier for this user's voice session. + string VoiceSessionId { get; } + } +} diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index d650c29bf..1e0a621a1 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -30,7 +30,7 @@ namespace Discord var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); Update(model, UpdateSource.Rest); - } + } public async Task Modify(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 7efe5239f..6e1282933 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -17,9 +17,9 @@ namespace Discord public override DiscordClient Discord { get; } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - public virtual Game? Game => null; public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); + public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Unknown; public User(DiscordClient discord, Model model) diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 6a8d4790a..1c0520a5a 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -13,7 +13,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; - public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); + public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) : base(discord, recipient, model) @@ -21,11 +21,11 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult(Members); + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult>(Members); public override Task> GetUsers(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public IUser GetCachedUser(ulong id) + public ICachedUser GetCachedUser(ulong id) { var currentUser = Discord.CurrentUser; if (id == Recipient.Id) @@ -48,7 +48,7 @@ namespace Discord { return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(IUser author, MessageModel model) + public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 8890c8230..f8a4aa380 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -11,6 +11,8 @@ using ExtendedModel = Discord.API.Gateway.ExtendedGuild; using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; +using RoleModel = Discord.API.Role; +using VoiceStateModel = Discord.API.VoiceState; namespace Discord { @@ -20,9 +22,11 @@ namespace Discord private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _presences; - private int _userCount; + private ConcurrentDictionary _voiceStates; public bool Available { get; private set; } //TODO: Add to IGuild + public int MemberCount { get; private set; } + public int DownloadedMemberCount { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task DownloaderPromise => _downloaderPromise.Task; @@ -32,9 +36,10 @@ namespace Discord public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); - public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) + public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { _downloaderPromise = new TaskCompletionSource(); + Update(model, UpdateSource.Creation, dataStore); } public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) @@ -52,6 +57,8 @@ namespace Discord _presences = new ConcurrentDictionary(); if (_roles == null) _roles = new ConcurrentDictionary(); + if (_voiceStates == null) + _voiceStates = new ConcurrentDictionary(); if (Emojis == null) Emojis = ImmutableArray.Create(); if (Features == null) @@ -61,7 +68,7 @@ namespace Discord base.Update(model as Model, source); - _userCount = model.MemberCount; + MemberCount = model.MemberCount; var channels = new ConcurrentHashSet(); if (model.Channels != null) @@ -75,7 +82,7 @@ namespace Discord if (model.Presences != null) { for (int i = 0; i < model.Presences.Length; i++) - AddCachedPresence(model.Presences[i], presences); + AddOrUpdateCachedPresence(model.Presences[i], presences); } _presences = presences; @@ -85,10 +92,19 @@ namespace Discord for (int i = 0; i < model.Members.Length; i++) AddCachedUser(model.Members[i], members, dataStore); _downloaderPromise = new TaskCompletionSource(); + DownloadedMemberCount = model.Members.Length; if (!model.Large) _downloaderPromise.SetResult(true); } _members = members; + + var voiceStates = new ConcurrentDictionary(); + if (model.VoiceStates != null) + { + for (int i = 0; i < model.VoiceStates.Length; i++) + AddOrUpdateCachedVoiceState(model.VoiceStates[i], _voiceStates); + } + _voiceStates = voiceStates; } public override Task GetChannel(ulong id) => Task.FromResult(GetCachedChannel(id)); @@ -108,7 +124,7 @@ namespace Discord (channels ?? _channels).TryRemove(id); } - public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) + public Presence AddOrUpdateCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) { var game = model.Game != null ? new Game(model.Game) : (Game?)null; var presence = new Presence(model.Status, game); @@ -130,6 +146,42 @@ namespace Discord return null; } + public Role AddCachedRole(RoleModel model, ConcurrentDictionary roles = null) + { + var role = new Role(this, model); + (roles ?? _roles)[model.Id] = role; + return role; + } + public Role RemoveCachedRole(ulong id) + { + Role role; + if (_roles.TryRemove(id, out role)) + return role; + return null; + } + + public VoiceState AddOrUpdateCachedVoiceState(VoiceStateModel model, ConcurrentDictionary voiceStates = null) + { + var voiceChannel = GetCachedChannel(model.ChannelId.Value) as CachedVoiceChannel; + var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + (voiceStates ?? _voiceStates)[model.UserId] = voiceState; + return voiceState; + } + public VoiceState? GetCachedVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + public VoiceState? RemoveCachedVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); public override Task GetCurrentUser() => Task.FromResult(CurrentUser); @@ -140,10 +192,11 @@ namespace Discord => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary members = null, DataStore dataStore = null) { - var user = Discord.AddCachedUser(model.User); + var user = Discord.GetOrAddCachedUser(model.User); var member = new CachedGuildUser(this, user, model); (members ?? _members)[user.Id] = member; user.AddRef(); + DownloadedMemberCount++; return member; } public CachedGuildUser GetCachedUser(ulong id) @@ -160,7 +213,6 @@ namespace Discord return member; return null; } - public async Task DownloadMembers() { if (!HasAllMembers) @@ -169,7 +221,7 @@ namespace Discord } public void CompleteDownloadMembers() { - _downloaderPromise.SetResult(true); + _downloaderPromise.TrySetResult(true); } public CachedGuild Clone() => MemberwiseClone() as CachedGuild; diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 191fbe05f..8801d59d7 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -2,11 +2,21 @@ namespace Discord { - internal class CachedGuildUser : GuildUser, ICachedEntity + internal class CachedGuildUser : GuildUser, ICachedUser { - public VoiceChannel VoiceChannel { get; private set; } - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public new CachedGuild Guild => base.Guild as CachedGuild; + public new CachedPublicUser User => base.User as CachedPublicUser; + + public Presence? Presence => Guild.GetCachedPresence(Id); + public override Game? Game => Presence?.Game; + public override UserStatus Status => Presence?.Status ?? UserStatus.Offline; + + public VoiceState? VoiceState => Guild.GetCachedVoiceState(Id); + public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) : base(guild, user, model) @@ -14,5 +24,6 @@ namespace Discord } public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 04842ab8a..d1c67dac7 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -1,15 +1,20 @@ using ChannelModel = Discord.API.Channel; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord { - internal class CachedPublicUser : User, ICachedEntity + internal class CachedPublicUser : User, ICachedUser { private int _references; + private Game? _game; + private UserStatus _status; public CachedDMChannel DMChannel { get; private set; } public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; + public override UserStatus Status => _status; + public override Game? Game => _game; public CachedPublicUser(DiscordSocketClient discord, Model model) : base(discord, model) @@ -39,6 +44,16 @@ namespace Discord } } + public void Update(PresenceModel model, UpdateSource source) + { + if (source == UpdateSource.Rest) return; + + var game = model.Game != null ? new Game(model.Game) : (Game?)null; + + _status = model.Status; + _game = game; + } + public void AddRef() { lock (this) @@ -54,5 +69,6 @@ namespace Discord } public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs index fe4a264c8..9b3543c11 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedSelfUser.cs @@ -2,7 +2,7 @@ namespace Discord { - internal class CachedSelfUser : SelfUser, ICachedEntity + internal class CachedSelfUser : SelfUser, ICachedUser { public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; @@ -12,5 +12,6 @@ namespace Discord } public CachedSelfUser Clone() => MemberwiseClone() as CachedSelfUser; + ICachedUser ICachedUser.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 96899a022..95c0ac375 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -14,7 +14,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; - public IReadOnlyCollection Members + public IReadOnlyCollection Members => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); public CachedTextChannel(CachedGuild guild, Model model) @@ -23,11 +23,11 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult(Members); + public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsers() => Task.FromResult>(Members); public override Task> GetUsers(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public IGuildUser GetCachedUser(ulong id) + public CachedGuildUser GetCachedUser(ulong id) { var user = Guild.GetCachedUser(id); if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) @@ -48,7 +48,7 @@ namespace Discord return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(IUser author, MessageModel model) + public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); @@ -65,10 +65,10 @@ namespace Discord public CachedTextChannel Clone() => MemberwiseClone() as CachedTextChannel; - IReadOnlyCollection ICachedMessageChannel.Members => Members; + IReadOnlyCollection ICachedMessageChannel.Members => Members; IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); - IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + ICachedUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs index 5db4a28a7..30ca49022 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs @@ -5,12 +5,12 @@ namespace Discord { internal interface ICachedMessageChannel : ICachedChannel, IMessageChannel { - IReadOnlyCollection Members { get; } + IReadOnlyCollection Members { get; } - CachedMessage AddCachedMessage(IUser author, MessageModel model); + CachedMessage AddCachedMessage(ICachedUser author, MessageModel model); new CachedMessage GetCachedMessage(ulong id); CachedMessage RemoveCachedMessage(ulong id); - IUser GetCachedUser(ulong id); + ICachedUser GetCachedUser(ulong id); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedUser.cs b/src/Discord.Net/Entities/WebSocket/ICachedUser.cs new file mode 100644 index 000000000..e9e7d2929 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/ICachedUser.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + internal interface ICachedUser : IUser, ICachedEntity + { + ICachedUser Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old b/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old deleted file mode 100644 index 0937f5049..000000000 --- a/src/Discord.Net/Entities/WebSocket/IVoiceState.cs.old +++ /dev/null @@ -1,62 +0,0 @@ -/*using System; -using Model = Discord.API.MemberVoiceState; - -namespace Discord.WebSocket -{ - internal class VoiceState : IVoiceState - { - [Flags] - private enum VoiceStates : byte - { - None = 0x0, - Muted = 0x01, - Deafened = 0x02, - Suppressed = 0x4, - SelfMuted = 0x10, - SelfDeafened = 0x20, - } - - private VoiceStates _voiceStates; - - public Guild Guild { get; } - public ulong UserId { get; } - - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel { get; set; } - - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened => (_voiceStates & VoiceStates.SelfDeafened) != 0; - /// Returns true if the guild is blocking audio from this user. - public bool IsMuted => (_voiceStates & VoiceStates.Muted) != 0; - /// Returns true if the guild is blocking audio to this user. - public bool IsDeafened => (_voiceStates & VoiceStates.Deafened) != 0; - /// Returns true if the guild is temporarily blocking audio to/from this user. - public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; - - public VoiceState(ulong userId, Guild guild) - { - UserId = userId; - Guild = guild; - } - - private void Update(Model model, UpdateSource source) - { - if (model.IsMuted == true) - _voiceStates |= VoiceStates.Muted; - else if (model.IsMuted == false) - _voiceStates &= ~VoiceStates.Muted; - - if (model.IsDeafened == true) - _voiceStates |= VoiceStates.Deafened; - else if (model.IsDeafened == false) - _voiceStates &= ~VoiceStates.Deafened; - - if (model.IsSuppressed == true) - _voiceStates |= VoiceStates.Suppressed; - else if (model.IsSuppressed == false) - _voiceStates &= ~VoiceStates.Suppressed; - } - } -}*/ \ No newline at end of file diff --git a/src/Discord.Net/Entities/WebSocket/VoiceState.cs b/src/Discord.Net/Entities/WebSocket/VoiceState.cs new file mode 100644 index 000000000..fc183a520 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/VoiceState.cs @@ -0,0 +1,42 @@ +using System; + +namespace Discord +{ + internal struct VoiceState : IVoiceState + { + [Flags] + private enum Flags : byte + { + None = 0x0, + Suppressed = 0x1, + SelfMuted = 0x2, + SelfDeafened = 0x4, + } + + private readonly Flags _voiceStates; + + public CachedVoiceChannel VoiceChannel { get; } + public string VoiceSessionId { get; } + + public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + + public VoiceState(CachedVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) + { + VoiceChannel = voiceChannel; + VoiceSessionId = sessionId; + + Flags voiceStates = Flags.None; + if (isSelfMuted) + voiceStates |= Flags.SelfMuted; + if (isSelfDeafened) + voiceStates |= Flags.SelfDeafened; + if (isSuppressed) + voiceStates |= Flags.Suppressed; + _voiceStates = voiceStates; + } + + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; + } +} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index a15ef430a..4da1b01d3 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -15,6 +15,7 @@ "buildOptions": { "allowUnsafe": true, + "define": [ "BENCHMARK" ], "warningsAsErrors": false }, From 1ea7fcb2c43d21d96c63a9e556869828b7897e9e Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 9 Jun 2016 02:29:24 -0300 Subject: [PATCH 017/160] Refactored to use TAP naming scheme --- src/Discord.Net/API/DiscordAPIClient.cs | 470 +++++++++--------- src/Discord.Net/DiscordClient.cs | 100 ++-- src/Discord.Net/DiscordSocketClient.cs | 224 ++++----- .../Entities/Channels/DMChannel.cs | 52 +- .../Entities/Channels/GuildChannel.cs | 48 +- src/Discord.Net/Entities/Channels/IChannel.cs | 6 +- .../Entities/Channels/IDMChannel.cs | 2 +- .../Entities/Channels/IGuildChannel.cs | 18 +- .../Entities/Channels/IMessageChannel.cs | 16 +- .../Entities/Channels/ITextChannel.cs | 2 +- .../Entities/Channels/IVoiceChannel.cs | 2 +- .../Entities/Channels/TextChannel.cs | 48 +- .../Entities/Channels/VoiceChannel.cs | 10 +- src/Discord.Net/Entities/Guilds/Guild.cs | 108 ++-- .../Entities/Guilds/GuildIntegration.cs | 12 +- src/Discord.Net/Entities/Guilds/IGuild.cs | 44 +- src/Discord.Net/Entities/Guilds/UserGuild.cs | 8 +- src/Discord.Net/Entities/IDeletable.cs | 2 +- src/Discord.Net/Entities/IUpdateable.cs | 2 +- src/Discord.Net/Entities/Invites/IInvite.cs | 2 +- src/Discord.Net/Entities/Invites/Invite.cs | 8 +- src/Discord.Net/Entities/Messages/IMessage.cs | 2 +- src/Discord.Net/Entities/Messages/Message.cs | 16 +- src/Discord.Net/Entities/Roles/IRole.cs | 2 +- src/Discord.Net/Entities/Roles/Role.cs | 8 +- src/Discord.Net/Entities/Users/GuildUser.cs | 18 +- src/Discord.Net/Entities/Users/IGuildUser.cs | 4 +- src/Discord.Net/Entities/Users/ISelfUser.cs | 2 +- src/Discord.Net/Entities/Users/IUser.cs | 2 +- src/Discord.Net/Entities/Users/SelfUser.cs | 8 +- src/Discord.Net/Entities/Users/User.cs | 4 +- .../Entities/WebSocket/CachedDMChannel.cs | 18 +- .../Entities/WebSocket/CachedGuild.cs | 16 +- .../Entities/WebSocket/CachedTextChannel.cs | 18 +- .../Entities/WebSocket/CachedVoiceChannel.cs | 6 +- .../Extensions/DiscordClientExtensions.cs | 4 +- src/Discord.Net/Extensions/EventExtensions.cs | 10 +- src/Discord.Net/Extensions/GuildExtensions.cs | 8 +- src/Discord.Net/IDiscordClient.cs | 34 +- src/Discord.Net/Logging/ILogger.cs | 36 +- src/Discord.Net/Logging/LogManager.cs | 144 +++--- src/Discord.Net/Logging/Logger.cs | 80 +-- src/Discord.Net/Net/Queue/IQueuedRequest.cs | 2 +- src/Discord.Net/Net/Queue/RequestQueue.cs | 12 +- .../Net/Queue/RequestQueueBucket.cs | 18 +- src/Discord.Net/Net/Queue/RestRequest.cs | 8 +- src/Discord.Net/Net/Queue/WebSocketRequest.cs | 4 +- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 14 +- src/Discord.Net/Net/Rest/IRestClient.cs | 6 +- .../Net/WebSockets/DefaultWebsocketClient.cs | 16 +- .../Net/WebSockets/IWebSocketClient.cs | 6 +- src/Discord.Net/Utilities/MessageCache.cs | 8 +- 52 files changed, 859 insertions(+), 859 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index c6007d2dd..95b331b1e 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -66,14 +66,14 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) { var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + await ReceivedGatewayEvent.RaiseAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); - await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + await ReceivedGatewayEvent.RaiseAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); }; } @@ -93,19 +93,19 @@ namespace Discord.API } public void Dispose() => Dispose(true); - public async Task Login(TokenType tokenType, string token, RequestOptions options = null) + public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, options).ConfigureAwait(false); + await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(TokenType tokenType, string token, RequestOptions options = null) + private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null) { if (LoginState != LoginState.LoggedOut) - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); LoginState = LoginState.LoggingIn; try @@ -115,7 +115,7 @@ namespace Discord.API AuthTokenType = TokenType.User; _authToken = null; _restClient.SetHeader("authorization", null); - await _requestQueue.SetCancelToken(_loginCancelToken.Token).ConfigureAwait(false); + await _requestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); _restClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; @@ -139,21 +139,21 @@ namespace Discord.API } catch (Exception) { - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); throw; } } - public async Task Logout() + public async Task LogoutAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LogoutInternal() + private async Task LogoutInternalAsync() { //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; @@ -162,25 +162,25 @@ namespace Discord.API try { _loginCancelToken?.Cancel(false); } catch { } - await DisconnectInternal().ConfigureAwait(false); - await _requestQueue.Clear().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); + await _requestQueue.ClearAsync().ConfigureAwait(false); - await _requestQueue.SetCancelToken(CancellationToken.None).ConfigureAwait(false); + await _requestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); _restClient.SetCancelToken(CancellationToken.None); LoginState = LoginState.LoggedOut; } - public async Task Connect() + public async Task ConnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternal().ConfigureAwait(false); + await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternal() + private async Task ConnectInternalAsync() { if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting."); @@ -194,29 +194,29 @@ namespace Discord.API if (_gatewayClient != null) _gatewayClient.SetCancelToken(_connectCancelToken.Token); - var gatewayResponse = await GetGateway().ConfigureAwait(false); + var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); var url = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; - await _gatewayClient.Connect(url).ConfigureAwait(false); + await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } catch (Exception) { - await DisconnectInternal().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); throw; } } - public async Task Disconnect() + public async Task DisconnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternal().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternal() + private async Task DisconnectInternalAsync() { if (_gatewayClient == null) throw new NotSupportedException("This client is not configured with websocket support."); @@ -227,105 +227,105 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } - await _gatewayClient.Disconnect().ConfigureAwait(false); + await _gatewayClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } //Core - public Task Send(string method, string endpoint, + public Task SendAsync(string method, string endpoint, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) - => SendInternal(method, endpoint, null, true, bucket, options); - public Task Send(string method, string endpoint, object payload, + => SendInternalAsync(method, endpoint, null, true, bucket, options); + public Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) - => SendInternal(method, endpoint, payload, true, bucket, options); - public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + => SendInternalAsync(method, endpoint, payload, true, bucket, options); + public Task SendAsync(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) - => SendInternal(method, endpoint, multipartArgs, true, bucket, options); - public async Task Send(string method, string endpoint, + => SendInternalAsync(method, endpoint, multipartArgs, true, bucket, options); + public async Task SendAsync(string method, string endpoint, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, options).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = + => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, bucket, options).ConfigureAwait(false)); + public async Task SendAsync(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, options).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, bucket, options).ConfigureAwait(false)); + public async Task SendAsync(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GlobalBucket bucket = GlobalBucket.GeneralRest, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, options).ConfigureAwait(false)); + => DeserializeJson(await SendInternalAsync(method, endpoint, multipartArgs, false, bucket, options).ConfigureAwait(false)); - public Task Send(string method, string endpoint, + public Task SendAsync(string method, string endpoint, GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendInternal(method, endpoint, null, true, bucket, guildId, options); - public Task Send(string method, string endpoint, object payload, + => SendInternalAsync(method, endpoint, null, true, bucket, guildId, options); + public Task SendAsync(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendInternal(method, endpoint, payload, true, bucket, guildId, options); - public Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + => SendInternalAsync(method, endpoint, payload, true, bucket, guildId, options); + public Task SendAsync(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendInternal(method, endpoint, multipartArgs, true, bucket, guildId, options); - public async Task Send(string method, string endpoint, + => SendInternalAsync(method, endpoint, multipartArgs, true, bucket, guildId, options); + public async Task SendAsync(string method, string endpoint, GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket, guildId, options).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, object payload, + => DeserializeJson(await SendInternalAsync(method, endpoint, null, false, bucket, guildId, options).ConfigureAwait(false)); + public async Task SendAsync(string method, string endpoint, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, payload, false, bucket, guildId, options).ConfigureAwait(false)); - public async Task Send(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, + => DeserializeJson(await SendInternalAsync(method, endpoint, payload, false, bucket, guildId, options).ConfigureAwait(false)); + public async Task SendAsync(string method, string endpoint, Stream file, IReadOnlyDictionary multipartArgs, GuildBucket bucket, ulong guildId, RequestOptions options = null) where TResponse : class - => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId, options).ConfigureAwait(false)); + => DeserializeJson(await SendInternalAsync(method, endpoint, multipartArgs, false, bucket, guildId, options).ConfigureAwait(false)); - private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + private Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, GlobalBucket bucket, RequestOptions options) - => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0, options); - private Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + => SendInternalAsync(method, endpoint, payload, headerOnly, BucketGroup.Global, (int)bucket, 0, options); + private Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, GuildBucket bucket, ulong guildId, RequestOptions options) - => SendInternal(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + => SendInternalAsync(method, endpoint, payload, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); + private Task SendInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GlobalBucket bucket, RequestOptions options) - => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0, options); - private Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + => SendInternalAsync(method, endpoint, multipartArgs, headerOnly, BucketGroup.Global, (int)bucket, 0, options); + private Task SendInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, GuildBucket bucket, ulong guildId, RequestOptions options) - => SendInternal(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); + => SendInternalAsync(method, endpoint, multipartArgs, headerOnly, BucketGroup.Guild, (int)bucket, guildId, options); - private async Task SendInternal(string method, string endpoint, object payload, bool headerOnly, + private async Task SendInternalAsync(string method, string endpoint, object payload, bool headerOnly, BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); string json = null; if (payload != null) json = SerializeJson(payload); - var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, json, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); + await SentRequest.RaiseAsync(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } - private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, + private async Task SendInternalAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId, RequestOptions options = null) { var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.SendAsync(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly, options), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); + await SentRequest.RaiseAsync(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } - public Task SendGateway(GatewayOpCode opCode, object payload, + public Task SendGatewayAsync(GatewayOpCode opCode, object payload, GlobalBucket bucket = GlobalBucket.GeneralGateway, RequestOptions options = null) - => SendGateway(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); - public Task SendGateway(GatewayOpCode opCode, object payload, + => SendGatewayAsync(opCode, payload, BucketGroup.Global, (int)bucket, 0, options); + public Task SendGatewayAsync(GatewayOpCode opCode, object payload, GuildBucket bucket, ulong guildId, RequestOptions options = null) - => SendGateway(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); - private async Task SendGateway(GatewayOpCode opCode, object payload, + => SendGatewayAsync(opCode, payload, BucketGroup.Guild, (int)bucket, guildId, options); + private async Task SendGatewayAsync(GatewayOpCode opCode, object payload, BucketGroup group, int bucketId, ulong guildId, RequestOptions options) { //TODO: Add ETF @@ -333,22 +333,22 @@ namespace Discord.API payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - await _requestQueue.Send(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); - await SentGatewayMessage.Raise((int)opCode).ConfigureAwait(false); + await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); + await SentGatewayMessage.RaiseAsync((int)opCode).ConfigureAwait(false); } //Auth - public async Task ValidateToken(RequestOptions options = null) + public async Task ValidateTokenAsync(RequestOptions options = null) { - await Send("GET", "auth/login", options: options).ConfigureAwait(false); + await SendAsync("GET", "auth/login", options: options).ConfigureAwait(false); } //Gateway - public async Task GetGateway(RequestOptions options = null) + public async Task GetGatewayAsync(RequestOptions options = null) { - return await Send("GET", "gateway", options: options).ConfigureAwait(false); + return await SendAsync("GET", "gateway", options: options).ConfigureAwait(false); } - public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) + public async Task SendIdentifyAsync(int largeThreshold = 100, bool useCompression = true, RequestOptions options = null) { var props = new Dictionary { @@ -361,82 +361,82 @@ namespace Discord.API LargeThreshold = largeThreshold, UseCompression = useCompression }; - await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } - public async Task SendHeartbeat(int lastSeq, RequestOptions options = null) + public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) { - await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } - public async Task SendRequestMembers(IEnumerable guildIds, RequestOptions options = null) + public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) { - await SendGateway(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); + await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); } //Channels - public async Task GetChannel(ulong channelId, RequestOptions options = null) + public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { - return await Send("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetChannel(ulong guildId, ulong channelId, RequestOptions options = null) + public async Task GetChannelAsync(ulong guildId, ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { - var model = await Send("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); + var model = await SendAsync("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); if (model.GuildId != guildId) return null; return model; } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildChannels(ulong guildId, RequestOptions options = null) + public async Task> GetGuildChannelsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/channels", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/channels", options: options).ConfigureAwait(false); } - public async Task CreateGuildChannel(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null) + public async Task CreateGuildChannelAsync(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - return await Send("POST", $"guilds/{guildId}/channels", args, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"guilds/{guildId}/channels", args, options: options).ConfigureAwait(false); } - public async Task DeleteChannel(ulong channelId, RequestOptions options = null) + public async Task DeleteChannelAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - return await Send("DELETE", $"channels/{channelId}", options: options).ConfigureAwait(false); + return await SendAsync("DELETE", $"channels/{channelId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyGuildChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyTextChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannel(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) + public async Task ModifyGuildChannelAsync(ulong channelId, ModifyVoiceChannelParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -445,9 +445,9 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - return await Send("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"channels/{channelId}", args, options: options).ConfigureAwait(false); } - public async Task ModifyGuildChannels(ulong guildId, IEnumerable args, RequestOptions options = null) + public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -458,58 +458,58 @@ namespace Discord.API case 0: return; case 1: - await ModifyGuildChannel(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); + await ModifyGuildChannelAsync(channels[0].Id, new ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false); break; default: - await Send("PATCH", $"guilds/{guildId}/channels", channels, options: options).ConfigureAwait(false); + await SendAsync("PATCH", $"guilds/{guildId}/channels", channels, options: options).ConfigureAwait(false); break; } } //Channel Permissions - public async Task ModifyChannelPermissions(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) + public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); - await Send("PUT", $"channels/{channelId}/permissions/{targetId}", args, options: options).ConfigureAwait(false); + await SendAsync("PUT", $"channels/{channelId}/permissions/{targetId}", args, options: options).ConfigureAwait(false); } - public async Task DeleteChannelPermission(ulong channelId, ulong targetId, RequestOptions options = null) + public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) { - await Send("DELETE", $"channels/{channelId}/permissions/{targetId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"channels/{channelId}/permissions/{targetId}", options: options).ConfigureAwait(false); } //Guilds - public async Task GetGuild(ulong guildId, RequestOptions options = null) + public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { - return await Send("GET", $"guilds/{guildId}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"guilds/{guildId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task CreateGuild(CreateGuildParams args, RequestOptions options = null) + public async Task CreateGuildAsync(CreateGuildParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); Preconditions.NotNullOrWhitespace(args.Region, nameof(args.Region)); - return await Send("POST", "guilds", args, options: options).ConfigureAwait(false); + return await SendAsync("POST", "guilds", args, options: options).ConfigureAwait(false); } - public async Task DeleteGuild(ulong guildId, RequestOptions options = null) + public async Task DeleteGuildAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("DELETE", $"guilds/{guildId}", options: options).ConfigureAwait(false); + return await SendAsync("DELETE", $"guilds/{guildId}", options: options).ConfigureAwait(false); } - public async Task LeaveGuild(ulong guildId, RequestOptions options = null) + public async Task LeaveGuildAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("DELETE", $"users/@me/guilds/{guildId}", options: options).ConfigureAwait(false); + return await SendAsync("DELETE", $"users/@me/guilds/{guildId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuild(ulong guildId, ModifyGuildParams args, RequestOptions options = null) + public async Task ModifyGuildAsync(ulong guildId, ModifyGuildParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -520,91 +520,91 @@ namespace Discord.API Preconditions.NotNull(args.Region, nameof(args.Region)); Preconditions.AtLeast(args.VerificationLevel, 0, nameof(args.VerificationLevel)); - return await Send("PATCH", $"guilds/{guildId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"guilds/{guildId}", args, options: options).ConfigureAwait(false); } - public async Task BeginGuildPrune(ulong guildId, GuildPruneParams args, RequestOptions options = null) + public async Task BeginGuildPruneAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - return await Send("POST", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); } - public async Task GetGuildPruneCount(ulong guildId, GuildPruneParams args, RequestOptions options = null) + public async Task GetGuildPruneCountAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Days, 0, nameof(args.Days)); - return await Send("GET", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); + return await SendAsync("GET", $"guilds/{guildId}/prune", args, options: options).ConfigureAwait(false); } //Guild Bans - public async Task> GetGuildBans(ulong guildId, RequestOptions options = null) + public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/bans", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/bans", options: options).ConfigureAwait(false); } - public async Task CreateGuildBan(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) + public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.PruneDays, 0, nameof(args.PruneDays)); - await Send("PUT", $"guilds/{guildId}/bans/{userId}", args, options: options).ConfigureAwait(false); + await SendAsync("PUT", $"guilds/{guildId}/bans/{userId}", args, options: options).ConfigureAwait(false); } - public async Task RemoveGuildBan(ulong guildId, ulong userId, RequestOptions options = null) + public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); - await Send("DELETE", $"guilds/{guildId}/bans/{userId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"guilds/{guildId}/bans/{userId}", options: options).ConfigureAwait(false); } //Guild Embeds - public async Task GetGuildEmbed(ulong guildId, RequestOptions options = null) + public async Task GetGuildEmbedAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { - return await Send("GET", $"guilds/{guildId}/embed", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"guilds/{guildId}/embed", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task ModifyGuildEmbed(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) + public async Task ModifyGuildEmbedAsync(ulong guildId, ModifyGuildEmbedParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("PATCH", $"guilds/{guildId}/embed", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"guilds/{guildId}/embed", args, options: options).ConfigureAwait(false); } //Guild Integrations - public async Task> GetGuildIntegrations(ulong guildId, RequestOptions options = null) + public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); } - public async Task CreateGuildIntegration(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) + public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); - return await Send("POST", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); + return await SendAsync("POST", $"guilds/{guildId}/integrations", options: options).ConfigureAwait(false); } - public async Task DeleteGuildIntegration(ulong guildId, ulong integrationId, RequestOptions options = null) + public async Task DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - return await Send("DELETE", $"guilds/{guildId}/integrations/{integrationId}", options: options).ConfigureAwait(false); + return await SendAsync("DELETE", $"guilds/{guildId}/integrations/{integrationId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildIntegration(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) + public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); @@ -612,74 +612,74 @@ namespace Discord.API Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior)); Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod)); - return await Send("PATCH", $"guilds/{guildId}/integrations/{integrationId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"guilds/{guildId}/integrations/{integrationId}", args, options: options).ConfigureAwait(false); } - public async Task SyncGuildIntegration(ulong guildId, ulong integrationId, RequestOptions options = null) + public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - return await Send("POST", $"guilds/{guildId}/integrations/{integrationId}/sync", options: options).ConfigureAwait(false); + return await SendAsync("POST", $"guilds/{guildId}/integrations/{integrationId}/sync", options: options).ConfigureAwait(false); } //Guild Invites - public async Task GetInvite(string inviteIdOrXkcd, RequestOptions options = null) + public async Task GetInviteAsync(string inviteIdOrXkcd, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteIdOrXkcd, nameof(inviteIdOrXkcd)); try { - return await Send("GET", $"invites/{inviteIdOrXkcd}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"invites/{inviteIdOrXkcd}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildInvites(ulong guildId, RequestOptions options = null) + public async Task> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/invites", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/invites", options: options).ConfigureAwait(false); } - public async Task GetChannelInvites(ulong channelId, RequestOptions options = null) + public async Task GetChannelInvitesAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - return await Send("GET", $"channels/{channelId}/invites", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"channels/{channelId}/invites", options: options).ConfigureAwait(false); } - public async Task CreateChannelInvite(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) + public async Task CreateChannelInviteAsync(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); - return await Send("POST", $"channels/{channelId}/invites", args, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"channels/{channelId}/invites", args, options: options).ConfigureAwait(false); } - public async Task DeleteInvite(string inviteCode, RequestOptions options = null) + public async Task DeleteInviteAsync(string inviteCode, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - return await Send("DELETE", $"invites/{inviteCode}", options: options).ConfigureAwait(false); + return await SendAsync("DELETE", $"invites/{inviteCode}", options: options).ConfigureAwait(false); } - public async Task AcceptInvite(string inviteCode, RequestOptions options = null) + public async Task AcceptInviteAsync(string inviteCode, RequestOptions options = null) { Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); - await Send("POST", $"invites/{inviteCode}", options: options).ConfigureAwait(false); + await SendAsync("POST", $"invites/{inviteCode}", options: options).ConfigureAwait(false); } //Guild Members - public async Task GetGuildMember(ulong guildId, ulong userId, RequestOptions options = null) + public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); try { - return await Send("GET", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> GetGuildMembers(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) + public async Task> GetGuildMembersAsync(ulong guildId, GetGuildMembersParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -699,7 +699,7 @@ namespace Discord.API { int runLimit = (limit >= DiscordConfig.MaxUsersPerBatch) ? DiscordConfig.MaxUsersPerBatch : limit; string endpoint = $"guilds/{guildId}/members?limit={runLimit}&offset={offset}"; - var models = await Send("GET", endpoint, options: options).ConfigureAwait(false); + var models = await SendAsync("GET", endpoint, options: options).ConfigureAwait(false); //Was this an empty batch? if (models.Length == 0) break; @@ -720,43 +720,43 @@ namespace Discord.API else return ImmutableArray.Create(); } - public async Task RemoveGuildMember(ulong guildId, ulong userId, RequestOptions options = null) + public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); - await Send("DELETE", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"guilds/{guildId}/members/{userId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) + public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, ModifyGuildMemberParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); - await Send("PATCH", $"guilds/{guildId}/members/{userId}", args, GuildBucket.ModifyMember, guildId, options: options).ConfigureAwait(false); + await SendAsync("PATCH", $"guilds/{guildId}/members/{userId}", args, GuildBucket.ModifyMember, guildId, options: options).ConfigureAwait(false); } //Guild Roles - public async Task> GetGuildRoles(ulong guildId, RequestOptions options = null) + public async Task> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); } - public async Task CreateGuildRole(ulong guildId, RequestOptions options = null) + public async Task CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send("POST", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); + return await SendAsync("POST", $"guilds/{guildId}/roles", options: options).ConfigureAwait(false); } - public async Task DeleteGuildRole(ulong guildId, ulong roleId, RequestOptions options = null) + public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); - await Send("DELETE", $"guilds/{guildId}/roles/{roleId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"guilds/{guildId}/roles/{roleId}", options: options).ConfigureAwait(false); } - public async Task ModifyGuildRole(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) + public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, ModifyGuildRoleParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(roleId, 0, nameof(roleId)); @@ -765,9 +765,9 @@ namespace Discord.API Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); - return await Send("PATCH", $"guilds/{guildId}/roles/{roleId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"guilds/{guildId}/roles/{roleId}", args, options: options).ConfigureAwait(false); } - public async Task> ModifyGuildRoles(ulong guildId, IEnumerable args, RequestOptions options = null) + public async Task> ModifyGuildRolesAsync(ulong guildId, IEnumerable args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotNull(args, nameof(args)); @@ -778,20 +778,20 @@ namespace Discord.API case 0: return ImmutableArray.Create(); case 1: - return ImmutableArray.Create(await ModifyGuildRole(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); + return ImmutableArray.Create(await ModifyGuildRoleAsync(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); default: - return await Send>("PATCH", $"guilds/{guildId}/roles", args, options: options).ConfigureAwait(false); + return await SendAsync>("PATCH", $"guilds/{guildId}/roles", args, options: options).ConfigureAwait(false); } } //Messages - public async Task GetChannelMessage(ulong channelId, ulong messageId, RequestOptions options = null) + public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); //TODO: Improve when Discord adds support - var msgs = await GetChannelMessages(channelId, new GetChannelMessagesParams { Limit = 1, RelativeDirection = Direction.Before, RelativeMessageId = messageId + 1 }).ConfigureAwait(false); + var msgs = await GetChannelMessagesAsync(channelId, new GetChannelMessagesParams { Limit = 1, RelativeDirection = Direction.Before, RelativeMessageId = messageId + 1 }).ConfigureAwait(false); return msgs.FirstOrDefault(); /*try @@ -800,7 +800,7 @@ namespace Discord.API } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; }*/ } - public async Task> GetChannelMessages(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) + public async Task> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -823,7 +823,7 @@ namespace Discord.API endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; else endpoint = $"channels/{channelId}/messages?limit={runCount}"; - var models = await Send("GET", endpoint, options: options).ConfigureAwait(false); + var models = await SendAsync("GET", endpoint, options: options).ConfigureAwait(false); //Was this an empty batch? if (models.Length == 0) break; @@ -847,17 +847,17 @@ namespace Discord.API else return ImmutableArray.Create(); } - public Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) + public Task CreateMessageAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return CreateMessageInternal(guildId, channelId, args); + return CreateMessageInternalAsync(guildId, channelId, args); } - public Task CreateDMMessage(ulong channelId, CreateMessageParams args, RequestOptions options = null) + public Task CreateDMMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { - return CreateMessageInternal(0, channelId, args); + return CreateMessageInternalAsync(0, channelId, args); } - public async Task CreateMessageInternal(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) + public async Task CreateMessageInternalAsync(ulong guildId, ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -866,21 +866,21 @@ namespace Discord.API throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); if (guildId != 0) - return await Send("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); } - public Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) + public Task UploadFileAsync(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return UploadFileInternal(guildId, channelId, file, args); + return UploadFileInternalAsync(guildId, channelId, file, args); } - public Task UploadDMFile(ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) + public Task UploadDMFileAsync(ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { - return UploadFileInternal(0, channelId, file, args); + return UploadFileInternalAsync(0, channelId, file, args); } - private async Task UploadFileInternal(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) + private async Task UploadFileInternalAsync(ulong guildId, ulong channelId, Stream file, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -892,41 +892,41 @@ namespace Discord.API } if (guildId != 0) - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GlobalBucket.DirectMessage, options: options).ConfigureAwait(false); } - public Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) + public Task DeleteMessageAsync(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return DeleteMessageInternal(guildId, channelId, messageId); + return DeleteMessageInternalAsync(guildId, channelId, messageId); } - public Task DeleteDMMessage(ulong channelId, ulong messageId, RequestOptions options = null) + public Task DeleteDMMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { - return DeleteMessageInternal(0, channelId, messageId); + return DeleteMessageInternalAsync(0, channelId, messageId); } - private async Task DeleteMessageInternal(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) + private async Task DeleteMessageInternalAsync(ulong guildId, ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); if (guildId != 0) - await Send("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId, options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"channels/{channelId}/messages/{messageId}", GuildBucket.DeleteMessage, guildId, options: options).ConfigureAwait(false); else - await Send("DELETE", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); } - public Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) + public Task DeleteMessagesAsync(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return DeleteMessagesInternal(guildId, channelId, args); + return DeleteMessagesInternalAsync(guildId, channelId, args); } - public Task DeleteDMMessages(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) + public Task DeleteDMMessagesAsync(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { - return DeleteMessagesInternal(0, channelId, args); + return DeleteMessagesInternalAsync(0, channelId, args); } - private async Task DeleteMessagesInternal(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) + private async Task DeleteMessagesInternalAsync(ulong guildId, ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); @@ -940,27 +940,27 @@ namespace Discord.API case 0: return; case 1: - await DeleteMessageInternal(guildId, channelId, messageIds[0]).ConfigureAwait(false); + await DeleteMessageInternalAsync(guildId, channelId, messageIds[0]).ConfigureAwait(false); break; default: if (guildId != 0) - await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId, options: options).ConfigureAwait(false); + await SendAsync("POST", $"channels/{channelId}/messages/bulk_delete", args, GuildBucket.DeleteMessages, guildId, options: options).ConfigureAwait(false); else - await Send("POST", $"channels/{channelId}/messages/bulk_delete", args, options: options).ConfigureAwait(false); + await SendAsync("POST", $"channels/{channelId}/messages/bulk_delete", args, options: options).ConfigureAwait(false); break; } } - public Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + public Task ModifyMessageAsync(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return ModifyMessageInternal(guildId, channelId, messageId, args); + return ModifyMessageInternalAsync(guildId, channelId, messageId, args); } - public Task ModifyDMMessage(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + public Task ModifyDMMessageAsync(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { - return ModifyMessageInternal(0, channelId, messageId, args); + return ModifyMessageInternalAsync(0, channelId, messageId, args); } - private async Task ModifyMessageInternal(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + private async Task ModifyMessageInternalAsync(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); @@ -973,104 +973,104 @@ namespace Discord.API } if (guildId != 0) - return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId, options: options).ConfigureAwait(false); else - return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", $"channels/{channelId}/messages/{messageId}", args, options: options).ConfigureAwait(false); } - public async Task AckMessage(ulong channelId, ulong messageId, RequestOptions options = null) + public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); - await Send("POST", $"channels/{channelId}/messages/{messageId}/ack", options: options).ConfigureAwait(false); + await SendAsync("POST", $"channels/{channelId}/messages/{messageId}/ack", options: options).ConfigureAwait(false); } - public async Task TriggerTypingIndicator(ulong channelId, RequestOptions options = null) + public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); - await Send("POST", $"channels/{channelId}/typing", options: options).ConfigureAwait(false); + await SendAsync("POST", $"channels/{channelId}/typing", options: options).ConfigureAwait(false); } //Users - public async Task GetUser(ulong userId, RequestOptions options = null) + public async Task GetUserAsync(ulong userId, RequestOptions options = null) { Preconditions.NotEqual(userId, 0, nameof(userId)); try { - return await Send("GET", $"users/{userId}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"users/{userId}", options: options).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task GetUser(string username, string discriminator, RequestOptions options = null) + public async Task GetUserAsync(string username, string discriminator, RequestOptions options = null) { Preconditions.NotNullOrEmpty(username, nameof(username)); Preconditions.NotNullOrEmpty(discriminator, nameof(discriminator)); try { - var models = await QueryUsers($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); + var models = await QueryUsersAsync($"{username}#{discriminator}", 1, options: options).ConfigureAwait(false); return models.FirstOrDefault(); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - public async Task> QueryUsers(string query, int limit, RequestOptions options = null) + public async Task> QueryUsersAsync(string query, int limit, RequestOptions options = null) { Preconditions.NotNullOrEmpty(query, nameof(query)); Preconditions.AtLeast(limit, 0, nameof(limit)); - return await Send>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}", options: options).ConfigureAwait(false); } //Current User/DMs - public async Task GetCurrentUser(RequestOptions options = null) + public async Task GetSelfAsync(RequestOptions options = null) { - return await Send("GET", "users/@me", options: options).ConfigureAwait(false); + return await SendAsync("GET", "users/@me", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserConnections(RequestOptions options = null) + public async Task> GetMyConnectionsAsync(RequestOptions options = null) { - return await Send>("GET", "users/@me/connections", options: options).ConfigureAwait(false); + return await SendAsync>("GET", "users/@me/connections", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserDMs(RequestOptions options = null) + public async Task> GetMyDMsAsync(RequestOptions options = null) { - return await Send>("GET", "users/@me/channels", options: options).ConfigureAwait(false); + return await SendAsync>("GET", "users/@me/channels", options: options).ConfigureAwait(false); } - public async Task> GetCurrentUserGuilds(RequestOptions options = null) + public async Task> GetMyGuildsAsync(RequestOptions options = null) { - return await Send>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); + return await SendAsync>("GET", "users/@me/guilds", options: options).ConfigureAwait(false); } - public async Task ModifyCurrentUser(ModifyCurrentUserParams args, RequestOptions options = null) + public async Task ModifySelfAsync(ModifyCurrentUserParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotNullOrEmpty(args.Username, nameof(args.Username)); - return await Send("PATCH", "users/@me", args, options: options).ConfigureAwait(false); + return await SendAsync("PATCH", "users/@me", args, options: options).ConfigureAwait(false); } - public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) + public async Task ModifyMyNickAsync(ulong guildId, ModifyCurrentUserNickParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEmpty(args.Nickname, nameof(args.Nickname)); - await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args, options: options).ConfigureAwait(false); + await SendAsync("PATCH", $"guilds/{guildId}/members/@me/nick", args, options: options).ConfigureAwait(false); } - public async Task CreateDMChannel(CreateDMChannelParams args, RequestOptions options = null) + public async Task CreateDMChannelAsync(CreateDMChannelParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(args.RecipientId, 0, nameof(args.RecipientId)); - return await Send("POST", $"users/@me/channels", args, options: options).ConfigureAwait(false); + return await SendAsync("POST", $"users/@me/channels", args, options: options).ConfigureAwait(false); } //Voice Regions - public async Task> GetVoiceRegions(RequestOptions options = null) + public async Task> GetVoiceRegionsAsync(RequestOptions options = null) { - return await Send>("GET", "voice/regions", options: options).ConfigureAwait(false); + return await SendAsync>("GET", "voice/regions", options: options).ConfigureAwait(false); } - public async Task> GetGuildVoiceRegions(ulong guildId, RequestOptions options = null) + public async Task> GetGuildVoiceRegionsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guilds/{guildId}/regions", options: options).ConfigureAwait(false); + return await SendAsync>("GET", $"guilds/{guildId}/regions", options: options).ConfigureAwait(false); } //Helpers diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 43829cca1..1fbd03953 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -35,7 +35,7 @@ namespace Discord public DiscordClient(DiscordConfig config) { _log = new LogManager(config.LogLevel); - _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); + _log.Message += async msg => await Log.RaiseAsync(msg).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); _restLogger = _log.CreateLogger("Rest"); @@ -44,34 +44,34 @@ namespace Discord //TODO: Is there any better way to do this WebSocketProvider access? ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); - ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + ApiClient.SentRequest += async (method, endpoint, millis) => await _log.VerboseAsync("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } /// - public async Task Login(TokenType tokenType, string token, bool validateToken = true) + public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); + await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) + private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) { if (LoginState != LoginState.LoggedOut) - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); LoginState = LoginState.LoggingIn; try { - await ApiClient.Login(tokenType, token).ConfigureAwait(false); + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); if (validateToken) { try { - await ApiClient.ValidateToken().ConfigureAwait(false); + await ApiClient.ValidateTokenAsync().ConfigureAwait(false); } catch (HttpException ex) { @@ -79,63 +79,63 @@ namespace Discord } } - await OnLogin().ConfigureAwait(false); + await OnLoginAsync().ConfigureAwait(false); LoginState = LoginState.LoggedIn; } catch (Exception) { - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); throw; } - await LoggedIn.Raise().ConfigureAwait(false); + await LoggedIn.RaiseAsync().ConfigureAwait(false); } - protected virtual Task OnLogin() => Task.CompletedTask; + protected virtual Task OnLoginAsync() => Task.CompletedTask; /// - public async Task Logout() + public async Task LogoutAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LogoutInternal().ConfigureAwait(false); + await LogoutInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LogoutInternal() + private async Task LogoutInternalAsync() { if (LoginState == LoginState.LoggedOut) return; LoginState = LoginState.LoggingOut; - await ApiClient.Logout().ConfigureAwait(false); + await ApiClient.LogoutAsync().ConfigureAwait(false); - await OnLogout().ConfigureAwait(false); + await OnLogoutAsync().ConfigureAwait(false); _currentUser = null; LoginState = LoginState.LoggedOut; - await LoggedOut.Raise().ConfigureAwait(false); + await LoggedOut.RaiseAsync().ConfigureAwait(false); } - protected virtual Task OnLogout() => Task.CompletedTask; + protected virtual Task OnLogoutAsync() => Task.CompletedTask; /// - public async Task> GetConnections() + public async Task> GetConnectionsAsync() { - var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); + var models = await ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); return models.Select(x => new Connection(x)).ToImmutableArray(); } /// - public virtual async Task GetChannel(ulong id) + public virtual async Task GetChannelAsync(ulong id) { - var model = await ApiClient.GetChannel(id).ConfigureAwait(false); + var model = await ApiClient.GetChannelAsync(id).ConfigureAwait(false); if (model != null) { if (model.GuildId != null) { - var guildModel = await ApiClient.GetGuild(model.GuildId.Value).ConfigureAwait(false); + var guildModel = await ApiClient.GetGuildAsync(model.GuildId.Value).ConfigureAwait(false); if (guildModel != null) { var guild = new Guild(this, guildModel); @@ -148,97 +148,97 @@ namespace Discord return null; } /// - public virtual async Task> GetDMChannels() + public virtual async Task> GetDMChannelsAsync() { - var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); + var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false); return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); } /// - public virtual async Task GetInvite(string inviteIdOrXkcd) + public virtual async Task GetInviteAsync(string inviteIdOrXkcd) { - var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); + var model = await ApiClient.GetInviteAsync(inviteIdOrXkcd).ConfigureAwait(false); if (model != null) return new Invite(this, model); return null; } /// - public virtual async Task GetGuild(ulong id) + public virtual async Task GetGuildAsync(ulong id) { - var model = await ApiClient.GetGuild(id).ConfigureAwait(false); + var model = await ApiClient.GetGuildAsync(id).ConfigureAwait(false); if (model != null) return new Guild(this, model); return null; } /// - public virtual async Task GetGuildEmbed(ulong id) + public virtual async Task GetGuildEmbedAsync(ulong id) { - var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); + var model = await ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); if (model != null) return new GuildEmbed(model); return null; } /// - public virtual async Task> GetGuilds() + public virtual async Task> GetGuildsAsync() { - var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); + var models = await ApiClient.GetMyGuildsAsync().ConfigureAwait(false); return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); } /// - public virtual async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) + public virtual async Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); - var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); + var model = await ApiClient.CreateGuildAsync(args).ConfigureAwait(false); return new Guild(this, model); } /// - public virtual async Task GetUser(ulong id) + public virtual async Task GetUserAsync(ulong id) { - var model = await ApiClient.GetUser(id).ConfigureAwait(false); + var model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); if (model != null) return new User(this, model); return null; } /// - public virtual async Task GetUser(string username, string discriminator) + public virtual async Task GetUserAsync(string username, string discriminator) { - var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); + var model = await ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); if (model != null) return new User(this, model); return null; } /// - public virtual async Task GetCurrentUser() + public virtual async Task GetCurrentUserAsync() { var user = _currentUser; if (user == null) { - var model = await ApiClient.GetCurrentUser().ConfigureAwait(false); + var model = await ApiClient.GetSelfAsync().ConfigureAwait(false); user = new SelfUser(this, model); _currentUser = user; } return user; } /// - public virtual async Task> QueryUsers(string query, int limit) + public virtual async Task> QueryUsersAsync(string query, int limit) { - var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); + var models = await ApiClient.QueryUsersAsync(query, limit).ConfigureAwait(false); return models.Select(x => new User(this, x)).ToImmutableArray(); } /// - public virtual async Task> GetVoiceRegions() + public virtual async Task> GetVoiceRegionsAsync() { - var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); } /// - public virtual async Task GetVoiceRegion(string id) + public virtual async Task GetVoiceRegionAsync(string id) { - var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); } @@ -251,7 +251,7 @@ namespace Discord public void Dispose() => Dispose(true); ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; - Task IDiscordClient.Connect() { throw new NotSupportedException(); } - Task IDiscordClient.Disconnect() { throw new NotSupportedException(); } + Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } + Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 449817b1c..eeb51f52a 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -115,38 +115,38 @@ namespace Discord _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Debug($"Sent {(GatewayOpCode)opCode}"); - ApiClient.ReceivedGatewayEvent += ProcessMessage; + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}"); + ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; GatewaySocket = config.WebSocketProvider(); _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); } - protected override async Task OnLogin() + protected override async Task OnLoginAsync() { - var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); + var voiceRegions = await ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); } - protected override async Task OnLogout() + protected override async Task OnLogoutAsync() { if (ConnectionState != ConnectionState.Disconnected) - await DisconnectInternal().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); _voiceRegions = ImmutableDictionary.Create(); } /// - public async Task Connect() + public async Task ConnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternal().ConfigureAwait(false); + await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternal() + private async Task ConnectInternalAsync() { if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting."); @@ -156,47 +156,47 @@ namespace Discord { _connectTask = new TaskCompletionSource(); _heartbeatCancelToken = new CancellationTokenSource(); - await ApiClient.Connect().ConfigureAwait(false); + await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } catch (Exception) { - await DisconnectInternal().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); throw; } - await Connected.Raise().ConfigureAwait(false); + await Connected.RaiseAsync().ConfigureAwait(false); } /// - public async Task Disconnect() + public async Task DisconnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternal().ConfigureAwait(false); + await DisconnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternal() + private async Task DisconnectInternalAsync() { ulong guildId; if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - await ApiClient.Disconnect().ConfigureAwait(false); + await ApiClient.DisconnectAsync().ConfigureAwait(false); await _heartbeatTask.ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; - await Disconnected.Raise().ConfigureAwait(false); + await Disconnected.RaiseAsync().ConfigureAwait(false); } /// - public override Task GetVoiceRegion(string id) + public override Task GetVoiceRegionAsync(string id) { VoiceRegion region; if (_voiceRegions.TryGetValue(id, out region)) @@ -205,7 +205,7 @@ namespace Discord } /// - public override Task GetGuild(ulong id) + public override Task GetGuildAsync(ulong id) { return Task.FromResult(DataStore.GetGuild(id)); } @@ -237,7 +237,7 @@ namespace Discord } /// - public override Task GetChannel(ulong id) + public override Task GetChannelAsync(ulong id) { return Task.FromResult(DataStore.GetChannel(id)); } @@ -284,12 +284,12 @@ namespace Discord } /// - public override Task GetUser(ulong id) + public override Task GetUserAsync(ulong id) { return Task.FromResult(DataStore.GetUser(id)); } /// - public override Task GetUser(string username, string discriminator) + public override Task GetUserAsync(string username, string discriminator) { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } @@ -310,10 +310,10 @@ namespace Discord } /// Downloads the members list for all large guilds. - public Task DownloadAllMembers() - => DownloadMembers(DataStore.Guilds.Where(x => !x.HasAllMembers)); + public Task DownloadAllMembersAsync() + => DownloadMembersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); /// Downloads the members list for the provided guilds, if they don't have a complete list. - public async Task DownloadMembers(IEnumerable guilds) + public async Task DownloadMembersAsync(IEnumerable guilds) { const short batchSize = 50; var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); @@ -321,7 +321,7 @@ namespace Discord return; else if (cachedGuilds.Length == 1) { - await cachedGuilds[0].DownloadMembers().ConfigureAwait(false); + await cachedGuilds[0].DownloadMembersAsync().ConfigureAwait(false); return; } @@ -341,7 +341,7 @@ namespace Discord batchTasks[j] = guild.DownloaderPromise; } - await ApiClient.SendRequestMembers(batchIds).ConfigureAwait(false); + await ApiClient.SendRequestMembersAsync(batchIds).ConfigureAwait(false); if (isLast && batchCount > 1) await Task.WhenAll(batchTasks.Take(count)).ConfigureAwait(false); @@ -350,7 +350,7 @@ namespace Discord } } - private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) + private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { #if BENCHMARK Stopwatch stopwatch = Stopwatch.StartNew(); @@ -365,22 +365,22 @@ namespace Discord { case GatewayOpCode.Hello: { - await _gatewayLogger.Debug($"Received Hello").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - await ApiClient.SendIdentify().ConfigureAwait(false); - _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); } break; case GatewayOpCode.HeartbeatAck: { - await _gatewayLogger.Debug($"Received HeartbeatAck").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received HeartbeatAck").ConfigureAwait(false); var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.Debug($"Latency = {latency} ms").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); Latency = latency; - await LatencyUpdated.Raise(latency).ConfigureAwait(false); + await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); } break; case GatewayOpCode.Dispatch: @@ -389,7 +389,7 @@ namespace Discord //Global case "READY": { - await _gatewayLogger.Debug($"Received Dispatch (READY)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (READY)").ConfigureAwait(false); //TODO: Make downloading large guilds optional var data = (payload as JToken).ToObject(_serializer); @@ -405,7 +405,7 @@ namespace Discord _sessionId = data.SessionId; DataStore = dataStore; - await Ready.Raise().ConfigureAwait(false); + await Ready.RaiseAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete } @@ -420,17 +420,17 @@ namespace Discord if (data.Unavailable == false) type = "GUILD_AVAILABLE"; - await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); if (data.Unavailable != false) - await JoinedGuild.Raise(guild).ConfigureAwait(false); + await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); - await GuildAvailable.Raise(guild).ConfigureAwait(false); + await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); } break; case "GUILD_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.Id); @@ -438,10 +438,10 @@ namespace Discord { var before = _enablePreUpdateEvents ? guild.Clone() : null; guild.Update(data, UpdateSource.WebSocket); - await GuildUpdated.Raise(before, guild).ConfigureAwait(false); + await GuildUpdated.RaiseAsync(before, guild).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild."); } break; case "GUILD_DELETE": @@ -449,26 +449,26 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); if (data.Unavailable == true) type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.Debug($"Received Dispatch ({type})").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); var guild = DataStore.RemoveGuild(data.Id); if (guild != null) { - await GuildUnavailable.Raise(guild).ConfigureAwait(false); + await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); if (data.Unavailable != true) - await LeftGuild.Raise(guild).ConfigureAwait(false); + await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); foreach (var member in guild.Members) member.User.RemoveRef(); } else - await _gatewayLogger.Warning($"{type} referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); } break; //Channels case "CHANNEL_CREATE": { - await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; @@ -481,17 +481,17 @@ namespace Discord DataStore.AddChannel(channel); } else - await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild."); } else channel = AddCachedDMChannel(data); if (channel != null) - await ChannelCreated.Raise(channel); + await ChannelCreated.RaiseAsync(channel); } break; case "CHANNEL_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.Id); @@ -499,44 +499,44 @@ namespace Discord { var before = _enablePreUpdateEvents ? channel.Clone() : null; channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.Raise(before, channel); + await ChannelUpdated.RaiseAsync(before, channel); } else - await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel."); } break; case "CHANNEL_DELETE": { - await _gatewayLogger.Debug($"Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = RemoveCachedChannel(data.Id); if (channel != null) - await ChannelDestroyed.Raise(channel); + await ChannelDestroyed.RaiseAsync(channel); else - await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel."); } break; //Members case "GUILD_MEMBER_ADD": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { var user = guild.AddCachedUser(data); - await UserJoined.Raise(user).ConfigureAwait(false); + await UserJoined.RaiseAsync(user).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild."); } break; case "GUILD_MEMBER_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -547,18 +547,18 @@ namespace Discord { var before = _enablePreUpdateEvents ? user.Clone() : null; user.Update(data, UpdateSource.WebSocket); - await UserUpdated.Raise(before, user); + await UserUpdated.RaiseAsync(before, user); } else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user."); } else - await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild."); } break; case "GUILD_MEMBER_REMOVE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -568,18 +568,18 @@ namespace Discord if (user != null) { user.User.RemoveRef(); - await UserLeft.Raise(user); + await UserLeft.RaiseAsync(user); } else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user."); } else - await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild."); } break; case "GUILD_MEMBERS_CHUNK": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -591,33 +591,33 @@ namespace Discord if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { guild.CompleteDownloadMembers(); - await GuildDownloadedMembers.Raise(guild).ConfigureAwait(false); + await GuildDownloadedMembers.RaiseAsync(guild).ConfigureAwait(false); } } else - await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild."); } break; //Roles case "GUILD_ROLE_CREATE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { var role = guild.AddCachedRole(data.Role); - await RoleCreated.Raise(role).ConfigureAwait(false); + await RoleCreated.RaiseAsync(role).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild."); } break; case "GUILD_ROLE_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -628,18 +628,18 @@ namespace Discord { var before = _enablePreUpdateEvents ? role.Clone() : null; role.Update(data.Role, UpdateSource.WebSocket); - await RoleUpdated.Raise(before, role).ConfigureAwait(false); + await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); } else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role."); } else - await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild."); } break; case "GUILD_ROLE_DELETE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -647,45 +647,45 @@ namespace Discord { var role = guild.RemoveCachedRole(data.RoleId); if (role != null) - await RoleDeleted.Raise(role).ConfigureAwait(false); + await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role."); } else - await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild."); } break; //Bans case "GUILD_BAN_ADD": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserBanned.Raise(new User(this, data)); + await UserBanned.RaiseAsync(new User(this, data)); else - await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild."); } break; case "GUILD_BAN_REMOVE": { - await _gatewayLogger.Debug($"Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserUnbanned.Raise(new User(this, data)); + await UserUnbanned.RaiseAsync(new User(this, data)); else - await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild."); } break; //Messages case "MESSAGE_CREATE": { - await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -696,18 +696,18 @@ namespace Discord if (author != null) { var msg = channel.AddCachedMessage(author, data); - await MessageReceived.Raise(msg).ConfigureAwait(false); + await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); } else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user."); } else - await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel."); } break; case "MESSAGE_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -716,32 +716,32 @@ namespace Discord var msg = channel.GetCachedMessage(data.Id); var before = _enablePreUpdateEvents ? msg.Clone() : null; msg.Update(data, UpdateSource.WebSocket); - await MessageUpdated.Raise(before, msg).ConfigureAwait(false); + await MessageUpdated.RaiseAsync(before, msg).ConfigureAwait(false); } else - await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel."); } break; case "MESSAGE_DELETE": { - await _gatewayLogger.Debug($"Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { var msg = channel.RemoveCachedMessage(data.Id); - await MessageDeleted.Raise(msg).ConfigureAwait(false); + await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); } else - await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel."); } break; //Statuses case "PRESENCE_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.GuildId == null) @@ -755,7 +755,7 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId.Value); if (guild == null) { - await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild."); break; } if (data.Status == UserStatus.Offline) @@ -767,7 +767,7 @@ namespace Discord break; case "TYPING_START": { - await _gatewayLogger.Debug($"Received Dispatch (TYPING_START)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -775,17 +775,17 @@ namespace Discord { var user = channel.GetCachedUser(data.UserId); if (user != null) - await UserIsTyping.Raise(channel, user).ConfigureAwait(false); + await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); } else - await _gatewayLogger.Warning("TYPING_START referenced an unknown channel.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("TYPING_START referenced an unknown channel.").ConfigureAwait(false); } break; //Voice case "VOICE_STATE_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.HasValue) @@ -802,7 +802,7 @@ namespace Discord user.Update(data, UpdateSource.WebSocket); } else - await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); } } break; @@ -810,14 +810,14 @@ namespace Discord //Settings case "USER_UPDATE": { - await _gatewayLogger.Debug($"Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Received Dispatch (USER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.Id == CurrentUser.Id) { var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; CurrentUser.Update(data, UpdateSource.WebSocket); - await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); + await CurrentUserUpdated.RaiseAsync(before, CurrentUser).ConfigureAwait(false); } } break; @@ -829,23 +829,23 @@ namespace Discord case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add case "VOICE_SERVER_UPDATE": //TODO: Add case "RESUMED": //TODO: Add - await _gatewayLogger.Debug($"Ignored Dispatch ({type})").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Ignored Dispatch ({type})").ConfigureAwait(false); return; //Others default: - await _gatewayLogger.Warning($"Unknown Dispatch ({type})").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); return; } break; default: - await _gatewayLogger.Warning($"Unknown OpCode ({opCode})").ConfigureAwait(false); + await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); return; } } catch (Exception ex) { - await _gatewayLogger.Error($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); return; } #if BENCHMARK @@ -854,11 +854,11 @@ namespace Discord { stopwatch.Stop(); double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - await _benchmarkLogger.Debug($"{millis} ms").ConfigureAwait(false); + await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); } #endif } - private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { try { @@ -868,7 +868,7 @@ namespace Discord //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect _heartbeatTime = Environment.TickCount; - await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index ef8c08c19..b1df139f0 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -33,21 +33,21 @@ namespace Discord Recipient.Update(model.Recipient, UpdateSource.Rest); } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Close() + public async Task CloseAsync() { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); } - public virtual async Task GetUser(ulong id) + public virtual async Task GetUserAsync(ulong id) { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); if (id == Recipient.Id) return Recipient; else if (id == currentUser.Id) @@ -55,66 +55,66 @@ namespace Discord else return null; } - public virtual async Task> GetUsers() + public virtual async Task> GetUsersAsync() { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); return ImmutableArray.Create(currentUser, Recipient); } - public virtual async Task> GetUsers(int limit, int offset) + public virtual async Task> GetUsersAsync(int limit, int offset) { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); + var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); return new IUser[] { currentUser, Recipient }.Skip(offset).Take(limit).ToImmutableArray(); } - public async Task SendMessage(string text, bool isTTS) + public async Task SendMessageAsync(string text, bool isTTS) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } - public async Task SendFile(string filePath, string text, bool isTTS) + public async Task SendFileAsync(string filePath, string text, bool isTTS) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } } - public async Task SendFile(Stream stream, string filename, string text, bool isTTS) + public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } - public virtual async Task GetMessage(ulong id) + public virtual async Task GetMessageAsync(ulong id) { - var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) return new Message(this, new User(Discord, model.Author), model); return null; } - public virtual async Task> GetMessages(int limit) + public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); } - public virtual async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); } - public async Task DeleteMessages(IEnumerable messages) + public async Task DeleteMessagesAsync(IEnumerable messages) { - await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessagesAsync(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } - public async Task TriggerTyping() + public async Task TriggerTypingAsync() { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); } public override string ToString() => '@' + Recipient.ToString(); diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 461f84076..2459b3103 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -46,37 +46,37 @@ namespace Discord _overwrites = newOverwrites; } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelAsync(Id).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildChannelParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelAsync(Id).ConfigureAwait(false); } - public abstract Task GetUser(ulong id); - public abstract Task> GetUsers(); - public abstract Task> GetUsers(int limit, int offset); + public abstract Task GetUserAsync(ulong id); + public abstract Task> GetUsersAsync(); + public abstract Task> GetUsersAsync(int limit, int offset); - public async Task> GetInvites() + public async Task> GetInvitesAsync() { - var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelInvitesAsync(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); } - public async Task CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) + public async Task CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) { var args = new CreateChannelInviteParams { @@ -85,7 +85,7 @@ namespace Discord Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInviteAsync(Id, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } @@ -104,28 +104,28 @@ namespace Discord return null; } - public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) + public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); } - public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) + public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); } - public async Task RemovePermissionOverwrite(IUser user) + public async Task RemovePermissionOverwriteAsync(IUser user) { - await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(user.Id, out value); } - public async Task RemovePermissionOverwrite(IRole role) + public async Task RemovePermissionOverwriteAsync(IRole role) { - await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(role.Id, out value); @@ -137,8 +137,8 @@ namespace Discord IGuild IGuildChannel.Guild => Guild; IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); - async Task IChannel.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false); - async Task> IChannel.GetUsers() => await GetUsers().ConfigureAwait(false); - async Task> IChannel.GetUsers(int limit, int offset) => await GetUsers(limit, offset).ConfigureAwait(false); + async Task IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); + async Task> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); + async Task> IChannel.GetUsersAsync(int limit, int offset) => await GetUsersAsync(limit, offset).ConfigureAwait(false); } } diff --git a/src/Discord.Net/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs index 13040dc78..b64e0f2c7 100644 --- a/src/Discord.Net/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -6,10 +6,10 @@ namespace Discord public interface IChannel : ISnowflakeEntity { /// Gets a collection of all users in this channel. - Task> GetUsers(); + Task> GetUsersAsync(); /// Gets a paginated collection of all users in this channel. - Task> GetUsers(int limit, int offset = 0); + Task> GetUsersAsync(int limit, int offset = 0); /// Gets a user in this channel with the provided id. - Task GetUser(ulong id); + Task GetUserAsync(ulong id); } } diff --git a/src/Discord.Net/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Entities/Channels/IDMChannel.cs index 5038bf36c..2714c59f5 100644 --- a/src/Discord.Net/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net/Entities/Channels/IDMChannel.cs @@ -8,6 +8,6 @@ namespace Discord IUser Recipient { get; } /// Closes this private channel, removing it from your channel list. - Task Close(); + Task CloseAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IGuildChannel.cs b/src/Discord.Net/Entities/Channels/IGuildChannel.cs index 81d4a0f2e..60f52d2b5 100644 --- a/src/Discord.Net/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/IGuildChannel.cs @@ -20,32 +20,32 @@ namespace Discord /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); + Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); /// Returns a collection of all invites to this channel. - Task> GetInvites(); + Task> GetInvitesAsync(); /// Gets a collection of permission overwrites for this channel. IReadOnlyCollection PermissionOverwrites { get; } /// Modifies this guild channel. - Task Modify(Action func); + Task ModifyAsync(Action func); /// Gets the permission overwrite for a specific role, or null if one does not exist. OverwritePermissions? GetPermissionOverwrite(IRole role); /// Gets the permission overwrite for a specific user, or null if one does not exist. OverwritePermissions? GetPermissionOverwrite(IUser user); /// Removes the permission overwrite for the given role, if one exists. - Task RemovePermissionOverwrite(IRole role); + Task RemovePermissionOverwriteAsync(IRole role); /// Removes the permission overwrite for the given user, if one exists. - Task RemovePermissionOverwrite(IUser user); + Task RemovePermissionOverwriteAsync(IUser user); /// Adds or updates the permission overwrite for the given role. - Task AddPermissionOverwrite(IRole role, OverwritePermissions permissions); + Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions); /// Adds or updates the permission overwrite for the given user. - Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions); + Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions); /// Gets a collection of all users in this channel. - new Task> GetUsers(); + new Task> GetUsersAsync(); /// Gets a user in this channel with the provided id. - new Task GetUser(ulong id); + new Task GetUserAsync(ulong id); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs index bb9015c1f..a5a73b177 100644 --- a/src/Discord.Net/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -10,24 +10,24 @@ namespace Discord IReadOnlyCollection CachedMessages { get; } /// Sends a message to this message channel. - Task SendMessage(string text, bool isTTS = false); + Task SendMessageAsync(string text, bool isTTS = false); /// Sends a file to this text channel, with an optional caption. - Task SendFile(string filePath, string text = null, bool isTTS = false); + Task SendFileAsync(string filePath, string text = null, bool isTTS = false); /// Sends a file to this text channel, with an optional caption. - Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false); + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false); /// Gets a message from this message channel with the given id, or null if not found. - Task GetMessage(ulong id); + Task GetMessageAsync(ulong id); /// Gets the message from this channel's cache with the given id, or null if not found. IMessage GetCachedMessage(ulong id); /// Gets the last N messages from this message channel. - Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch); + Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch); /// Gets a collection of messages in this channel. - Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); + Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); /// Bulk deletes multiple messages. - Task DeleteMessages(IEnumerable messages); + Task DeleteMessagesAsync(IEnumerable messages); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. - Task TriggerTyping(); + Task TriggerTypingAsync(); } } diff --git a/src/Discord.Net/Entities/Channels/ITextChannel.cs b/src/Discord.Net/Entities/Channels/ITextChannel.cs index fe0578e57..3b4248b6e 100644 --- a/src/Discord.Net/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net/Entities/Channels/ITextChannel.cs @@ -10,6 +10,6 @@ namespace Discord string Topic { get; } /// Modifies this text channel. - Task Modify(Action func); + Task ModifyAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs index d94a97a63..fc90b2935 100644 --- a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs @@ -12,6 +12,6 @@ namespace Discord int UserLimit { get; } /// Modifies this voice channel. - Task Modify(Action func); + Task ModifyAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 2c824ffa8..778eea0ac 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -30,83 +30,83 @@ namespace Discord base.Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); var args = new ModifyTextChannelParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public override async Task GetUser(ulong id) + public override async Task GetUserAsync(ulong id) { - var user = await Guild.GetUser(id).ConfigureAwait(false); + var user = await Guild.GetUserAsync(id).ConfigureAwait(false); if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) return user; return null; } - public override async Task> GetUsers() + public override async Task> GetUsersAsync() { - var users = await Guild.GetUsers().ConfigureAwait(false); + var users = await Guild.GetUsersAsync().ConfigureAwait(false); return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); } - public override async Task> GetUsers(int limit, int offset) + public override async Task> GetUsersAsync(int limit, int offset) { - var users = await Guild.GetUsers(limit, offset).ConfigureAwait(false); + var users = await Guild.GetUsersAsync(limit, offset).ConfigureAwait(false); return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); } - public async Task SendMessage(string text, bool isTTS) + public async Task SendMessageAsync(string text, bool isTTS) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateMessageAsync(Guild.Id, Id, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } - public async Task SendFile(string filePath, string text, bool isTTS) + public async Task SendFileAsync(string filePath, string text, bool isTTS) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, file, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } } - public async Task SendFile(Stream stream, string filename, string text, bool isTTS) + public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, stream, args).ConfigureAwait(false); return new Message(this, new User(Discord, model.Author), model); } - public virtual async Task GetMessage(ulong id) + public virtual async Task GetMessageAsync(ulong id) { - var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) return new Message(this, new User(Discord, model.Author), model); return null; } - public virtual async Task> GetMessages(int limit) + public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); } - public virtual async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); } - public async Task DeleteMessages(IEnumerable messages) + public async Task DeleteMessagesAsync(IEnumerable messages) { - await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessagesAsync(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } - public async Task TriggerTyping() + public async Task TriggerTypingAsync() { - await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicatorAsync(Id).ConfigureAwait(false); } private string DebuggerDisplay => $"{Name} ({Id}, Text)"; diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index 8947c9672..2d2d5ed63 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -26,25 +26,25 @@ namespace Discord UserLimit = model.UserLimit; } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); var args = new ModifyVoiceChannelParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannelAsync(Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public override Task GetUser(ulong id) + public override Task GetUserAsync(ulong id) { throw new NotSupportedException(); } - public override Task> GetUsers() + public override Task> GetUsersAsync() { throw new NotSupportedException(); } - public override Task> GetUsers(int limit, int offset) + public override Task> GetUsersAsync(int limit, int offset) { throw new NotSupportedException(); } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 65ce5ec71..c8e79238b 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -103,114 +103,114 @@ namespace Discord } } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); + var response = await Discord.ApiClient.GetGuildAsync(Id).ConfigureAwait(false); Update(response, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildParams(); func(args); - var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildAsync(Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task ModifyEmbed(Action func) + public async Task ModifyEmbedAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildEmbedParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildEmbedAsync(Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task ModifyChannels(IEnumerable args) + public async Task ModifyChannelsAsync(IEnumerable args) { //TODO: Update channels - await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannelsAsync(Id, args).ConfigureAwait(false); } - public async Task ModifyRoles(IEnumerable args) + public async Task ModifyRolesAsync(IEnumerable args) { - var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.ModifyGuildRolesAsync(Id, args).ConfigureAwait(false); Update(models, UpdateSource.Rest); } - public async Task Leave() + public async Task LeaveAsync() { - await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuildAsync(Id).ConfigureAwait(false); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildAsync(Id).ConfigureAwait(false); } - public async Task> GetBans() + public async Task> GetBansAsync() { - var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildBansAsync(Id).ConfigureAwait(false); return models.Select(x => new User(Discord, x)).ToImmutableArray(); } - public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); - public async Task AddBan(ulong userId, int pruneDays = 0) + public Task AddBanAsync(IUser user, int pruneDays = 0) => AddBanAsync(user, pruneDays); + public async Task AddBanAsync(ulong userId, int pruneDays = 0) { var args = new CreateGuildBanParams() { PruneDays = pruneDays }; - await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); + await Discord.ApiClient.CreateGuildBanAsync(Id, userId, args).ConfigureAwait(false); } - public Task RemoveBan(IUser user) => RemoveBan(user.Id); - public async Task RemoveBan(ulong userId) + public Task RemoveBanAsync(IUser user) => RemoveBanAsync(user.Id); + public async Task RemoveBanAsync(ulong userId) { - await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildBanAsync(Id, userId).ConfigureAwait(false); } - public virtual async Task GetChannel(ulong id) + public virtual async Task GetChannelAsync(ulong id) { - var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelAsync(Id, id).ConfigureAwait(false); if (model != null) return ToChannel(model); return null; } - public virtual async Task> GetChannels() + public virtual async Task> GetChannelsAsync() { - var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildChannelsAsync(Id).ConfigureAwait(false); return models.Select(x => ToChannel(x)).ToImmutableArray(); } - public async Task CreateTextChannel(string name) + public async Task CreateTextChannelAsync(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannelAsync(Id, args).ConfigureAwait(false); return new TextChannel(this, model); } - public async Task CreateVoiceChannel(string name) + public async Task CreateVoiceChannelAsync(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannelAsync(Id, args).ConfigureAwait(false); return new VoiceChannel(this, model); } - public async Task> GetIntegrations() + public async Task> GetIntegrationsAsync() { - var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildIntegrationsAsync(Id).ConfigureAwait(false); return models.Select(x => new GuildIntegration(this, x)).ToImmutableArray(); } - public async Task CreateIntegration(ulong id, string type) + public async Task CreateIntegrationAsync(ulong id, string type) { var args = new CreateGuildIntegrationParams { Id = id, Type = type }; - var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildIntegrationAsync(Id, args).ConfigureAwait(false); return new GuildIntegration(this, model); } - public async Task> GetInvites() + public async Task> GetInvitesAsync() { - var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildInvitesAsync(Id).ConfigureAwait(false); return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); } - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) + public async Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) { if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); @@ -222,7 +222,7 @@ namespace Discord Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateChannelInviteAsync(DefaultChannelId, args).ConfigureAwait(false); return new InviteMetadata(Discord, model); } @@ -233,14 +233,14 @@ namespace Discord return result; return null; } - public async Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) { if (name == null) throw new ArgumentNullException(nameof(name)); - var model = await Discord.ApiClient.CreateGuildRole(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildRoleAsync(Id).ConfigureAwait(false); var role = new Role(this, model); - await role.Modify(x => + await role.ModifyAsync(x => { x.Name = name; x.Permissions = (permissions ?? role.Permissions).RawValue; @@ -251,38 +251,38 @@ namespace Discord return role; } - public virtual async Task GetUser(ulong id) + public virtual async Task GetUserAsync(ulong id) { - var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMemberAsync(Id, id).ConfigureAwait(false); if (model != null) return new GuildUser(this, new User(Discord, model.User), model); return null; } - public virtual async Task GetCurrentUser() + public virtual async Task GetCurrentUserAsync() { - var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - return await GetUser(currentUser.Id).ConfigureAwait(false); + var currentUser = await Discord.GetCurrentUserAsync().ConfigureAwait(false); + return await GetUserAsync(currentUser.Id).ConfigureAwait(false); } - public virtual async Task> GetUsers() + public virtual async Task> GetUsersAsync() { var args = new GetGuildMembersParams(); - var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildMembersAsync(Id, args).ConfigureAwait(false); return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); } - public virtual async Task> GetUsers(int limit, int offset) + public virtual async Task> GetUsersAsync(int limit, int offset) { var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; - var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildMembersAsync(Id, args).ConfigureAwait(false); return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); } - public async Task PruneUsers(int days = 30, bool simulate = false) + public async Task PruneUsersAsync(int days = 30, bool simulate = false) { var args = new GuildPruneParams() { Days = days }; GetGuildPruneCountResponse model; if (simulate) - model = await Discord.ApiClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.GetGuildPruneCountAsync(Id, args).ConfigureAwait(false); else - model = await Discord.ApiClient.BeginGuildPrune(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.BeginGuildPruneAsync(Id, args).ConfigureAwait(false); return model.Pruned; } @@ -306,7 +306,7 @@ namespace Discord IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; - Task IGuild.DownloadUsers() { throw new NotSupportedException(); } + Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } IRole IGuild.GetRole(ulong id) => GetRole(id); } diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index 5dbdd6d47..913536fa6 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -47,23 +47,23 @@ namespace Discord User = new User(Discord, model.User); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildIntegrationParams(); func(args); - var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(Guild.Id, Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Sync() + public async Task SyncAsync() { - await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.SyncGuildIntegrationAsync(Guild.Id, Id).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 8d86dcd8b..9949b788d 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -40,59 +40,59 @@ namespace Discord IReadOnlyCollection Roles { get; } /// Modifies this guild. - Task Modify(Action func); + Task ModifyAsync(Action func); /// Modifies this guild's embed. - Task ModifyEmbed(Action func); + Task ModifyEmbedAsync(Action func); /// Bulk modifies the channels of this guild. - Task ModifyChannels(IEnumerable args); + Task ModifyChannelsAsync(IEnumerable args); /// Bulk modifies the roles of this guild. - Task ModifyRoles(IEnumerable args); + Task ModifyRolesAsync(IEnumerable args); /// Leaves this guild. If you are the owner, use Delete instead. - Task Leave(); + Task LeaveAsync(); /// Gets a collection of all users banned on this guild. - Task> GetBans(); + Task> GetBansAsync(); /// Bans the provided user from this guild and optionally prunes their recent messages. - Task AddBan(IUser user, int pruneDays = 0); + Task AddBanAsync(IUser user, int pruneDays = 0); /// Bans the provided user id from this guild and optionally prunes their recent messages. - Task AddBan(ulong userId, int pruneDays = 0); + Task AddBanAsync(ulong userId, int pruneDays = 0); /// Unbans the provided user if it is currently banned. - Task RemoveBan(IUser user); + Task RemoveBanAsync(IUser user); /// Unbans the provided user id if it is currently banned. - Task RemoveBan(ulong userId); + Task RemoveBanAsync(ulong userId); /// Gets a collection of all channels in this guild. - Task> GetChannels(); + Task> GetChannelsAsync(); /// Gets the channel in this guild with the provided id, or null if not found. - Task GetChannel(ulong id); + Task GetChannelAsync(ulong id); /// Creates a new text channel. - Task CreateTextChannel(string name); + Task CreateTextChannelAsync(string name); /// Creates a new voice channel. - Task CreateVoiceChannel(string name); + Task CreateVoiceChannelAsync(string name); /// Gets a collection of all invites to this guild. - Task> GetInvites(); + Task> GetInvitesAsync(); /// Creates a new invite to this guild. /// The time (in seconds) until the invite expires. Set to null to never expire. /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); + Task CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); /// Gets the role in this guild with the provided id, or null if not found. IRole GetRole(ulong id); /// Creates a new role. - Task CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); /// Gets a collection of all users in this guild. - Task> GetUsers(); + Task> GetUsersAsync(); /// Gets the user in this guild with the provided id, or null if not found. - Task GetUser(ulong id); + Task GetUserAsync(ulong id); /// Gets the current user for this guild. - Task GetCurrentUser(); + Task GetCurrentUserAsync(); /// Downloads all users for this guild if the current list is incomplete. - Task DownloadUsers(); + Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. - Task PruneUsers(int days = 30, bool simulate = false); + Task PruneUsersAsync(int days = 30, bool simulate = false); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Entities/Guilds/UserGuild.cs index a34b40d85..9d76817e5 100644 --- a/src/Discord.Net/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Entities/Guilds/UserGuild.cs @@ -33,13 +33,13 @@ namespace Discord Permissions = new GuildPermissions(model.Permissions); } - public async Task Leave() + public async Task LeaveAsync() { - await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuildAsync(Id).ConfigureAwait(false); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildAsync(Id).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net/Entities/IDeletable.cs b/src/Discord.Net/Entities/IDeletable.cs index 98887b571..f35f8ad88 100644 --- a/src/Discord.Net/Entities/IDeletable.cs +++ b/src/Discord.Net/Entities/IDeletable.cs @@ -5,6 +5,6 @@ namespace Discord public interface IDeletable { /// Deletes this object and all its children. - Task Delete(); + Task DeleteAsync(); } } diff --git a/src/Discord.Net/Entities/IUpdateable.cs b/src/Discord.Net/Entities/IUpdateable.cs index 4f7d5ed34..50b23bb95 100644 --- a/src/Discord.Net/Entities/IUpdateable.cs +++ b/src/Discord.Net/Entities/IUpdateable.cs @@ -5,6 +5,6 @@ namespace Discord public interface IUpdateable { /// Updates this object's properties with its current state. - Task Update(); + Task UpdateAsync(); } } diff --git a/src/Discord.Net/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs index eddc3df5a..d9da5b3ec 100644 --- a/src/Discord.Net/Entities/Invites/IInvite.cs +++ b/src/Discord.Net/Entities/Invites/IInvite.cs @@ -19,6 +19,6 @@ namespace Discord ulong GuildId { get; } /// Accepts this invite and joins the target guild. This will fail on bot accounts. - Task Accept(); + Task AcceptAsync(); } } diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs index d21b93331..90e380582 100644 --- a/src/Discord.Net/Entities/Invites/Invite.cs +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -37,13 +37,13 @@ namespace Discord ChannelName = model.Channel.Name; } - public async Task Accept() + public async Task AcceptAsync() { - await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); + await Discord.ApiClient.AcceptInviteAsync(Code).ConfigureAwait(false); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); + await Discord.ApiClient.DeleteInviteAsync(Code).ConfigureAwait(false); } public override string ToString() => XkcdUrl ?? Url; diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index d9cd9ab04..311eb17d5 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -34,6 +34,6 @@ namespace Discord IReadOnlyCollection MentionedUsers { get; } /// Modifies this message. - Task Modify(Action func); + Task ModifyAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index 4d05e409f..e72f89a57 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -97,14 +97,14 @@ namespace Discord Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetChannelMessage(Channel.Id, Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); @@ -114,18 +114,18 @@ namespace Discord Model model; if (guildChannel != null) - model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyMessageAsync(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else - model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyDMMessageAsync(Channel.Id, Id, args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Delete() + public async Task DeleteAsync() { var guildChannel = Channel as GuildChannel; if (guildChannel != null) - await Discord.ApiClient.DeleteMessage(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessageAsync(guildChannel.Id, Channel.Id, Id).ConfigureAwait(false); else - await Discord.ApiClient.DeleteDMMessage(Channel.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); } public override string ToString() => Text; diff --git a/src/Discord.Net/Entities/Roles/IRole.cs b/src/Discord.Net/Entities/Roles/IRole.cs index 36d0ce641..29975be46 100644 --- a/src/Discord.Net/Entities/Roles/IRole.cs +++ b/src/Discord.Net/Entities/Roles/IRole.cs @@ -24,6 +24,6 @@ namespace Discord ulong GuildId { get; } /// Modifies this role. - Task Modify(Action func); + Task ModifyAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs index 577a4c252..9511ce67e 100644 --- a/src/Discord.Net/Entities/Roles/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -44,18 +44,18 @@ namespace Discord Permissions = new GuildPermissions(model.Permissions.Value); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildRoleParams(); func(args); - var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); + var response = await Discord.ApiClient.ModifyGuildRoleAsync(Guild.Id, Id, args).ConfigureAwait(false); Update(response, UpdateSource.Rest); } - public async Task Delete() + public async Task DeleteAsync() { - await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildRoleAsync(Guild.Id, Id).ConfigureAwait(false); } public Role Clone() => MemberwiseClone() as Role; diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index dd879dd20..b520d301f 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -67,31 +67,31 @@ namespace Discord IsMute = model.Mute; } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMemberAsync(Guild.Id, Id).ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildMemberParams(); func(args); - bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; + bool isCurrentUser = (await Discord.GetCurrentUserAsync().ConfigureAwait(false)).Id == Id; if (isCurrentUser && args.Nickname.IsSpecified) { var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" }; - await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + await Discord.ApiClient.ModifyMyNickAsync(Guild.Id, nickArgs).ConfigureAwait(false); args.Nickname = new Optional(); //Remove } if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) { - await Discord.ApiClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildMemberAsync(Guild.Id, Id, args).ConfigureAwait(false); if (args.Deaf.IsSpecified) IsDeaf = args.Deaf.Value; if (args.Mute.IsSpecified) @@ -102,9 +102,9 @@ namespace Discord Roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } } - public async Task Kick() + public async Task KickAsync() { - await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildMemberAsync(Guild.Id, Id).ConfigureAwait(false); } public ChannelPermissions GetPermissions(IGuildChannel channel) @@ -113,7 +113,7 @@ namespace Discord return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); } - public Task CreateDMChannel() => User.CreateDMChannel(); + public Task CreateDMChannelAsync() => User.CreateDMChannelAsync(); IGuild IGuildUser.Guild => Guild; IReadOnlyCollection IGuildUser.Roles => Roles; diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 5b68af6ca..ad90b6901 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -28,8 +28,8 @@ namespace Discord ChannelPermissions GetPermissions(IGuildChannel channel); /// Kicks this user from this guild. - Task Kick(); + Task KickAsync(); /// Modifies this user's properties in this guild. - Task Modify(Action func); + Task ModifyAsync(Action func); } } diff --git a/src/Discord.Net/Entities/Users/ISelfUser.cs b/src/Discord.Net/Entities/Users/ISelfUser.cs index d6e7c7718..8efd0cef4 100644 --- a/src/Discord.Net/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net/Entities/Users/ISelfUser.cs @@ -11,6 +11,6 @@ namespace Discord /// Returns true if this user's email has been verified. bool IsVerified { get; } - Task Modify(Action func); + Task ModifyAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index 0b1b04332..9f2709a3d 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -15,6 +15,6 @@ namespace Discord //TODO: CreateDMChannel is a candidate to move to IGuildUser, and User made a common class, depending on next friends list update /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannel(); + Task CreateDMChannelAsync(); } } diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 1e0a621a1..bdd90d2ff 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -24,20 +24,20 @@ namespace Discord IsVerified = model.IsVerified; } - public async Task Update() + public async Task UpdateAsync() { if (IsAttached) throw new NotSupportedException(); - var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); + var model = await Discord.ApiClient.GetSelfAsync().ConfigureAwait(false); Update(model, UpdateSource.Rest); } - public async Task Modify(Action func) + public async Task ModifyAsync(Action func) { if (func != null) throw new NullReferenceException(nameof(func)); var args = new ModifyCurrentUserParams(); func(args); - var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } } diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 6e1282933..70dd158c5 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -38,10 +38,10 @@ namespace Discord Username = model.Username; } - public async Task CreateDMChannel() + public async Task CreateDMChannelAsync() { var args = new CreateDMChannelParams { RecipientId = Id }; - var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); return new DMChannel(Discord, this, model); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 1c0520a5a..2bba02a46 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -21,9 +21,9 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult>(Members); - public override Task> GetUsers(int limit, int offset) + public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsersAsync() => Task.FromResult>(Members); + public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); public ICachedUser GetCachedUser(ulong id) { @@ -36,17 +36,17 @@ namespace Discord return null; } - public override async Task GetMessage(ulong id) + public override async Task GetMessageAsync(ulong id) { - return await _messages.Download(id).ConfigureAwait(false); + return await _messages.DownloadAsync(id).ConfigureAwait(false); } - public override async Task> GetMessages(int limit) + public override async Task> GetMessagesAsync(int limit) { - return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); + return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); } - public override async Task> GetMessages(ulong fromMessageId, Direction dir, int limit) + public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { - return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); + return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); } public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) { diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index f8a4aa380..058cd5b52 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -107,8 +107,8 @@ namespace Discord _voiceStates = voiceStates; } - public override Task GetChannel(ulong id) => Task.FromResult(GetCachedChannel(id)); - public override Task> GetChannels() => Task.FromResult>(Channels); + public override Task GetChannelAsync(ulong id) => Task.FromResult(GetCachedChannel(id)); + public override Task> GetChannelsAsync() => Task.FromResult>(Channels); public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet channels = null) { var channel = ToChannel(model); @@ -182,13 +182,13 @@ namespace Discord return null; } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task GetCurrentUser() + public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task GetCurrentUserAsync() => Task.FromResult(CurrentUser); - public override Task> GetUsers() + public override Task> GetUsersAsync() => Task.FromResult>(Members); //TODO: Is there a better way of exposing pagination? - public override Task> GetUsers(int limit, int offset) + public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary members = null, DataStore dataStore = null) { @@ -213,10 +213,10 @@ namespace Discord return member; return null; } - public async Task DownloadMembers() + public async Task DownloadMembersAsync() { if (!HasAllMembers) - await Discord.ApiClient.SendRequestMembers(new ulong[] { Id }).ConfigureAwait(false); + await Discord.ApiClient.SendRequestMembersAsync(new ulong[] { Id }).ConfigureAwait(false); await _downloaderPromise.Task.ConfigureAwait(false); } public void CompleteDownloadMembers() diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 95c0ac375..cedd5208b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -23,9 +23,9 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() => Task.FromResult>(Members); - public override Task> GetUsers(int limit, int offset) + public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task> GetUsersAsync() => Task.FromResult>(Members); + public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser GetCachedUser(ulong id) { @@ -35,17 +35,17 @@ namespace Discord return null; } - public override async Task GetMessage(ulong id) + public override async Task GetMessageAsync(ulong id) { - return await _messages.Download(id).ConfigureAwait(false); + return await _messages.DownloadAsync(id).ConfigureAwait(false); } - public override async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + public override async Task> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch) { - return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); + return await _messages.DownloadAsync(null, Direction.Before, limit).ConfigureAwait(false); } - public override async Task> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public override async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { - return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); + return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); } public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index 3b9f42bec..22639eb16 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -19,11 +19,11 @@ namespace Discord { } - public override Task GetUser(ulong id) + public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); - public override Task> GetUsers() + public override Task> GetUsersAsync() => Task.FromResult(Members); - public override Task> GetUsers(int limit, int offset) + public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); public IGuildUser GetCachedUser(ulong id) { diff --git a/src/Discord.Net/Extensions/DiscordClientExtensions.cs b/src/Discord.Net/Extensions/DiscordClientExtensions.cs index 4d262715e..53e09c0e0 100644 --- a/src/Discord.Net/Extensions/DiscordClientExtensions.cs +++ b/src/Discord.Net/Extensions/DiscordClientExtensions.cs @@ -5,9 +5,9 @@ namespace Discord.Extensions { public static class DiscordClientExtensions { - public static async Task GetOptimalVoiceRegion(this DiscordClient discord) + public static async Task GetOptimalVoiceRegionAsync(this DiscordClient discord) { - var regions = await discord.GetVoiceRegions().ConfigureAwait(false); + var regions = await discord.GetVoiceRegionsAsync().ConfigureAwait(false); return regions.FirstOrDefault(x => x.IsOptimal); } } diff --git a/src/Discord.Net/Extensions/EventExtensions.cs b/src/Discord.Net/Extensions/EventExtensions.cs index 867d4d41d..4467af55c 100644 --- a/src/Discord.Net/Extensions/EventExtensions.cs +++ b/src/Discord.Net/Extensions/EventExtensions.cs @@ -7,7 +7,7 @@ namespace Discord.Extensions { //TODO: Optimize these for if there is only 1 subscriber (can we do this?) //TODO: Could we maintain our own list instead of generating one on every invocation? - public static async Task Raise(this Func eventHandler) + public static async Task RaiseAsync(this Func eventHandler) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) @@ -16,7 +16,7 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke().ConfigureAwait(false); } } - public static async Task Raise(this Func eventHandler, T arg) + public static async Task RaiseAsync(this Func eventHandler, T arg) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) @@ -25,7 +25,7 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke(arg).ConfigureAwait(false); } } - public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2) + public static async Task RaiseAsync(this Func eventHandler, T1 arg1, T2 arg2) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) @@ -34,7 +34,7 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke(arg1, arg2).ConfigureAwait(false); } } - public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) + public static async Task RaiseAsync(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) @@ -43,7 +43,7 @@ namespace Discord.Extensions await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3).ConfigureAwait(false); } } - public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + public static async Task RaiseAsync(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { var subscriptions = eventHandler?.GetInvocationList(); if (subscriptions != null) diff --git a/src/Discord.Net/Extensions/GuildExtensions.cs b/src/Discord.Net/Extensions/GuildExtensions.cs index a438994c9..8100ca4b5 100644 --- a/src/Discord.Net/Extensions/GuildExtensions.cs +++ b/src/Discord.Net/Extensions/GuildExtensions.cs @@ -4,9 +4,9 @@ namespace Discord.Extensions { public static class GuildExtensions { - public static async Task GetTextChannel(this IGuild guild, ulong id) - => await guild.GetChannel(id).ConfigureAwait(false) as ITextChannel; - public static async Task GetVoiceChannel(this IGuild guild, ulong id) - => await guild.GetChannel(id).ConfigureAwait(false) as IVoiceChannel; + public static async Task GetTextChannelAsync(this IGuild guild, ulong id) + => await guild.GetChannelAsync(id).ConfigureAwait(false) as ITextChannel; + public static async Task GetVoiceChannelAsync(this IGuild guild, ulong id) + => await guild.GetChannelAsync(id).ConfigureAwait(false) as IVoiceChannel; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index e3049c9c3..821de976f 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -13,29 +13,29 @@ namespace Discord DiscordApiClient ApiClient { get; } - Task Login(TokenType tokenType, string token, bool validateToken = true); - Task Logout(); + Task LoginAsync(TokenType tokenType, string token, bool validateToken = true); + Task LogoutAsync(); - Task Connect(); - Task Disconnect(); + Task ConnectAsync(); + Task DisconnectAsync(); - Task GetChannel(ulong id); - Task> GetDMChannels(); + Task GetChannelAsync(ulong id); + Task> GetDMChannelsAsync(); - Task> GetConnections(); + Task> GetConnectionsAsync(); - Task GetGuild(ulong id); - Task> GetGuilds(); - Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); + Task GetGuildAsync(ulong id); + Task> GetGuildsAsync(); + Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); - Task GetInvite(string inviteIdOrXkcd); + Task GetInviteAsync(string inviteIdOrXkcd); - Task GetUser(ulong id); - Task GetUser(string username, string discriminator); - Task GetCurrentUser(); - Task> QueryUsers(string query, int limit); + Task GetUserAsync(ulong id); + Task GetUserAsync(string username, string discriminator); + Task GetCurrentUserAsync(); + Task> QueryUsersAsync(string query, int limit); - Task> GetVoiceRegions(); - Task GetVoiceRegion(string id); + Task> GetVoiceRegionsAsync(); + Task GetVoiceRegionAsync(string id); } } diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs index ccc7f06f7..207c03dc7 100644 --- a/src/Discord.Net/Logging/ILogger.cs +++ b/src/Discord.Net/Logging/ILogger.cs @@ -7,28 +7,28 @@ namespace Discord.Logging { LogSeverity Level { get; } - Task Log(LogSeverity severity, string message, Exception exception = null); - Task Log(LogSeverity severity, FormattableString message, Exception exception = null); - Task Log(LogSeverity severity, Exception exception); + Task LogAsync(LogSeverity severity, string message, Exception exception = null); + Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null); + Task LogAsync(LogSeverity severity, Exception exception); - Task Error(string message, Exception exception = null); - Task Error(FormattableString message, Exception exception = null); - Task Error(Exception exception); + Task ErrorAsync(string message, Exception exception = null); + Task ErrorAsync(FormattableString message, Exception exception = null); + Task ErrorAsync(Exception exception); - Task Warning(string message, Exception exception = null); - Task Warning(FormattableString message, Exception exception = null); - Task Warning(Exception exception); + Task WarningAsync(string message, Exception exception = null); + Task WarningAsync(FormattableString message, Exception exception = null); + Task WarningAsync(Exception exception); - Task Info(string message, Exception exception = null); - Task Info(FormattableString message, Exception exception = null); - Task Info(Exception exception); + Task InfoAsync(string message, Exception exception = null); + Task InfoAsync(FormattableString message, Exception exception = null); + Task InfoAsync(Exception exception); - Task Verbose(string message, Exception exception = null); - Task Verbose(FormattableString message, Exception exception = null); - Task Verbose(Exception exception); + Task VerboseAsync(string message, Exception exception = null); + Task VerboseAsync(FormattableString message, Exception exception = null); + Task VerboseAsync(Exception exception); - Task Debug(string message, Exception exception = null); - Task Debug(FormattableString message, Exception exception = null); - Task Debug(Exception exception); + Task DebugAsync(string message, Exception exception = null); + Task DebugAsync(FormattableString message, Exception exception = null); + Task DebugAsync(Exception exception); } } diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 83e82824c..2d1ef53c7 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -15,101 +15,101 @@ namespace Discord.Logging Level = minSeverity; } - public async Task Log(LogSeverity severity, string source, string message, Exception ex = null) + public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } - public async Task Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) + public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); } - public async Task Log(LogSeverity severity, string source, Exception ex) + public async Task LogAsync(LogSeverity severity, string source, Exception ex) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } - async Task ILogger.Log(LogSeverity severity, string message, Exception ex) + async Task ILogger.LogAsync(LogSeverity severity, string message, Exception ex) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); } - async Task ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) + async Task ILogger.LogAsync(LogSeverity severity, FormattableString message, Exception ex) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); } - async Task ILogger.Log(LogSeverity severity, Exception ex) + async Task ILogger.LogAsync(LogSeverity severity, Exception ex) { if (severity <= Level) - await Message.Raise(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); + await Message.RaiseAsync(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); } - public Task Error(string source, string message, Exception ex = null) - => Log(LogSeverity.Error, source, message, ex); - public Task Error(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Error, source, message, ex); - public Task Error(string source, Exception ex) - => Log(LogSeverity.Error, source, ex); - Task ILogger.Error(string message, Exception ex) - => Log(LogSeverity.Error, "Discord", message, ex); - Task ILogger.Error(FormattableString message, Exception ex) - => Log(LogSeverity.Error, "Discord", message, ex); - Task ILogger.Error(Exception ex) - => Log(LogSeverity.Error, "Discord", ex); + public Task ErrorAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Error, source, message, ex); + public Task ErrorAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Error, source, message, ex); + public Task ErrorAsync(string source, Exception ex) + => LogAsync(LogSeverity.Error, source, ex); + Task ILogger.ErrorAsync(string message, Exception ex) + => LogAsync(LogSeverity.Error, "Discord", message, ex); + Task ILogger.ErrorAsync(FormattableString message, Exception ex) + => LogAsync(LogSeverity.Error, "Discord", message, ex); + Task ILogger.ErrorAsync(Exception ex) + => LogAsync(LogSeverity.Error, "Discord", ex); - public Task Warning(string source, string message, Exception ex = null) - => Log(LogSeverity.Warning, source, message, ex); - public Task Warning(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Warning, source, message, ex); - public Task Warning(string source, Exception ex) - => Log(LogSeverity.Warning, source, ex); - Task ILogger.Warning(string message, Exception ex) - => Log(LogSeverity.Warning, "Discord", message, ex); - Task ILogger.Warning(FormattableString message, Exception ex) - => Log(LogSeverity.Warning, "Discord", message, ex); - Task ILogger.Warning(Exception ex) - => Log(LogSeverity.Warning, "Discord", ex); + public Task WarningAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Warning, source, message, ex); + public Task WarningAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Warning, source, message, ex); + public Task WarningAsync(string source, Exception ex) + => LogAsync(LogSeverity.Warning, source, ex); + Task ILogger.WarningAsync(string message, Exception ex) + => LogAsync(LogSeverity.Warning, "Discord", message, ex); + Task ILogger.WarningAsync(FormattableString message, Exception ex) + => LogAsync(LogSeverity.Warning, "Discord", message, ex); + Task ILogger.WarningAsync(Exception ex) + => LogAsync(LogSeverity.Warning, "Discord", ex); - public Task Info(string source, string message, Exception ex = null) - => Log(LogSeverity.Info, source, message, ex); - public Task Info(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Info, source, message, ex); - public Task Info(string source, Exception ex) - => Log(LogSeverity.Info, source, ex); - Task ILogger.Info(string message, Exception ex) - => Log(LogSeverity.Info, "Discord", message, ex); - Task ILogger.Info(FormattableString message, Exception ex) - => Log(LogSeverity.Info, "Discord", message, ex); - Task ILogger.Info(Exception ex) - => Log(LogSeverity.Info, "Discord", ex); + public Task InfoAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Info, source, message, ex); + public Task InfoAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Info, source, message, ex); + public Task InfoAsync(string source, Exception ex) + => LogAsync(LogSeverity.Info, source, ex); + Task ILogger.InfoAsync(string message, Exception ex) + => LogAsync(LogSeverity.Info, "Discord", message, ex); + Task ILogger.InfoAsync(FormattableString message, Exception ex) + => LogAsync(LogSeverity.Info, "Discord", message, ex); + Task ILogger.InfoAsync(Exception ex) + => LogAsync(LogSeverity.Info, "Discord", ex); - public Task Verbose(string source, string message, Exception ex = null) - => Log(LogSeverity.Verbose, source, message, ex); - public Task Verbose(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Verbose, source, message, ex); - public Task Verbose(string source, Exception ex) - => Log(LogSeverity.Verbose, source, ex); - Task ILogger.Verbose(string message, Exception ex) - => Log(LogSeverity.Verbose, "Discord", message, ex); - Task ILogger.Verbose(FormattableString message, Exception ex) - => Log(LogSeverity.Verbose, "Discord", message, ex); - Task ILogger.Verbose(Exception ex) - => Log(LogSeverity.Verbose, "Discord", ex); + public Task VerboseAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Verbose, source, message, ex); + public Task VerboseAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Verbose, source, message, ex); + public Task VerboseAsync(string source, Exception ex) + => LogAsync(LogSeverity.Verbose, source, ex); + Task ILogger.VerboseAsync(string message, Exception ex) + => LogAsync(LogSeverity.Verbose, "Discord", message, ex); + Task ILogger.VerboseAsync(FormattableString message, Exception ex) + => LogAsync(LogSeverity.Verbose, "Discord", message, ex); + Task ILogger.VerboseAsync(Exception ex) + => LogAsync(LogSeverity.Verbose, "Discord", ex); - public Task Debug(string source, string message, Exception ex = null) - => Log(LogSeverity.Debug, source, message, ex); - public Task Debug(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Debug, source, message, ex); - public Task Debug(string source, Exception ex) - => Log(LogSeverity.Debug, source, ex); - Task ILogger.Debug(string message, Exception ex) - => Log(LogSeverity.Debug, "Discord", message, ex); - Task ILogger.Debug(FormattableString message, Exception ex) - => Log(LogSeverity.Debug, "Discord", message, ex); - Task ILogger.Debug(Exception ex) - => Log(LogSeverity.Debug, "Discord", ex); + public Task DebugAsync(string source, string message, Exception ex = null) + => LogAsync(LogSeverity.Debug, source, message, ex); + public Task DebugAsync(string source, FormattableString message, Exception ex = null) + => LogAsync(LogSeverity.Debug, source, message, ex); + public Task DebugAsync(string source, Exception ex) + => LogAsync(LogSeverity.Debug, source, ex); + Task ILogger.DebugAsync(string message, Exception ex) + => LogAsync(LogSeverity.Debug, "Discord", message, ex); + Task ILogger.DebugAsync(FormattableString message, Exception ex) + => LogAsync(LogSeverity.Debug, "Discord", message, ex); + Task ILogger.DebugAsync(Exception ex) + => LogAsync(LogSeverity.Debug, "Discord", ex); public Logger CreateLogger(string name) => new Logger(this, name); } diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 759917488..36897ea44 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Logging { - internal class Logger + internal class Logger : ILogger { private readonly LogManager _manager; @@ -16,44 +16,44 @@ namespace Discord.Logging Name = name; } - public Task Log(LogSeverity severity, string message, Exception exception = null) - => _manager.Log(severity, Name, message, exception); - public Task Log(LogSeverity severity, FormattableString message, Exception exception = null) - => _manager.Log(severity, Name, message, exception); - - public Task Error(string message, Exception exception = null) - => _manager.Error(Name, message, exception); - public Task Error(FormattableString message, Exception exception = null) - => _manager.Error(Name, message, exception); - public Task Error(Exception exception) - => _manager.Error(Name, exception); - - public Task Warning(string message, Exception exception = null) - => _manager.Warning(Name, message, exception); - public Task Warning(FormattableString message, Exception exception = null) - => _manager.Warning(Name, message, exception); - public Task Warning(Exception exception) - => _manager.Warning(Name, exception); - - public Task Info(string message, Exception exception = null) - => _manager.Info(Name, message, exception); - public Task Info(FormattableString message, Exception exception = null) - => _manager.Info(Name, message, exception); - public Task Info(Exception exception) - => _manager.Info(Name, exception); - - public Task Verbose(string message, Exception exception = null) - => _manager.Verbose(Name, message, exception); - public Task Verbose(FormattableString message, Exception exception = null) - => _manager.Verbose(Name, message, exception); - public Task Verbose(Exception exception) - => _manager.Verbose(Name, exception); - - public Task Debug(string message, Exception exception = null) - => _manager.Debug(Name, message, exception); - public Task Debug(FormattableString message, Exception exception = null) - => _manager.Debug(Name, message, exception); - public Task Debug(Exception exception) - => _manager.Debug(Name, exception); + public Task LogAsync(LogSeverity severity, string message, Exception exception = null) + => _manager.LogAsync(severity, Name, message, exception); + public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) + => _manager.LogAsync(severity, Name, message, exception); + + public Task ErrorAsync(string message, Exception exception = null) + => _manager.ErrorAsync(Name, message, exception); + public Task ErrorAsync(FormattableString message, Exception exception = null) + => _manager.ErrorAsync(Name, message, exception); + public Task ErrorAsync(Exception exception) + => _manager.ErrorAsync(Name, exception); + + public Task WarningAsync(string message, Exception exception = null) + => _manager.WarningAsync(Name, message, exception); + public Task WarningAsync(FormattableString message, Exception exception = null) + => _manager.WarningAsync(Name, message, exception); + public Task WarningAsync(Exception exception) + => _manager.WarningAsync(Name, exception); + + public Task InfoAsync(string message, Exception exception = null) + => _manager.InfoAsync(Name, message, exception); + public Task InfoAsync(FormattableString message, Exception exception = null) + => _manager.InfoAsync(Name, message, exception); + public Task InfoAsync(Exception exception) + => _manager.InfoAsync(Name, exception); + + public Task VerboseAsync(string message, Exception exception = null) + => _manager.VerboseAsync(Name, message, exception); + public Task VerboseAsync(FormattableString message, Exception exception = null) + => _manager.VerboseAsync(Name, message, exception); + public Task VerboseAsync(Exception exception) + => _manager.VerboseAsync(Name, exception); + + public Task DebugAsync(string message, Exception exception = null) + => _manager.DebugAsync(Name, message, exception); + public Task DebugAsync(FormattableString message, Exception exception = null) + => _manager.DebugAsync(Name, message, exception); + public Task DebugAsync(Exception exception) + => _manager.DebugAsync(Name, exception); } } diff --git a/src/Discord.Net/Net/Queue/IQueuedRequest.cs b/src/Discord.Net/Net/Queue/IQueuedRequest.cs index 099e0e7ed..ad0c8fcb6 100644 --- a/src/Discord.Net/Net/Queue/IQueuedRequest.cs +++ b/src/Discord.Net/Net/Queue/IQueuedRequest.cs @@ -11,6 +11,6 @@ namespace Discord.Net.Queue CancellationToken CancelToken { get; } int? TimeoutTick { get; } - Task Send(); + Task SendAsync(); } } diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index adf54af9c..631f5b457 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -61,7 +61,7 @@ namespace Discord.Net.Queue _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; } - public async Task SetCancelToken(CancellationToken cancelToken) + public async Task SetCancelTokenAsync(CancellationToken cancelToken) { await _lock.WaitAsync().ConfigureAwait(false); try @@ -72,17 +72,17 @@ namespace Discord.Net.Queue finally { _lock.Release(); } } - internal async Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong guildId) { request.CancelToken = _cancelToken; var bucket = GetBucket(group, bucketId, guildId); - return await bucket.Send(request).ConfigureAwait(false); + return await bucket.SendAsync(request).ConfigureAwait(false); } - internal async Task Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) { request.CancelToken = _cancelToken; var bucket = GetBucket(group, bucketId, guildId); - return await bucket.Send(request).ConfigureAwait(false); + return await bucket.SendAsync(request).ConfigureAwait(false); } private RequestQueueBucket CreateBucket(BucketDefinition def) @@ -119,7 +119,7 @@ namespace Discord.Net.Queue return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type])); } - public async Task Clear() + public async Task ClearAsync() { await _lock.WaitAsync().ConfigureAwait(false); try diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs index 6fc60d9ea..bfc7e2bb2 100644 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -28,13 +28,13 @@ namespace Discord.Net.Queue Parent = parent; } - public async Task Send(IQueuedRequest request) + public async Task SendAsync(IQueuedRequest request) { var endTick = request.TimeoutTick; //Wait until a spot is open in our bucket if (_semaphore != null) - await Enter(endTick).ConfigureAwait(false); + await EnterAsync(endTick).ConfigureAwait(false); try { while (true) @@ -63,10 +63,10 @@ namespace Discord.Net.Queue { //If there's a parent bucket, pass this request to them if (Parent != null) - return await Parent.Send(request).ConfigureAwait(false); + return await Parent.SendAsync(request).ConfigureAwait(false); //We have all our semaphores, send the request - return await request.Send().ConfigureAwait(false); + return await request.SendAsync().ConfigureAwait(false); } catch (HttpRateLimitException ex) { @@ -79,7 +79,7 @@ namespace Discord.Net.Queue { //Make sure we put this entry back after WindowMilliseconds if (_semaphore != null) - QueueExit(); + QueueExitAsync(); } } @@ -92,17 +92,17 @@ namespace Discord.Net.Queue { _resumeNotifier = new TaskCompletionSource(); _pauseEndTick = unchecked(Environment.TickCount + milliseconds); - QueueResume(milliseconds); + QueueResumeAsync(milliseconds); } } } - private async Task QueueResume(int millis) + private async Task QueueResumeAsync(int millis) { await Task.Delay(millis).ConfigureAwait(false); _resumeNotifier.SetResult(0); } - private async Task Enter(int? endTick) + private async Task EnterAsync(int? endTick) { if (endTick.HasValue) { @@ -113,7 +113,7 @@ namespace Discord.Net.Queue else await _semaphore.WaitAsync().ConfigureAwait(false); } - private async Task QueueExit() + private async Task QueueExitAsync() { await Task.Delay(_windowMilliseconds).ConfigureAwait(false); _semaphore.Release(); diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs index 778a03be3..aa63eacb5 100644 --- a/src/Discord.Net/Net/Queue/RestRequest.cs +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -47,14 +47,14 @@ namespace Discord.Net.Queue Promise = new TaskCompletionSource(); } - public async Task Send() + public async Task SendAsync() { if (IsMultipart) - return await Client.Send(Method, Endpoint, MultipartParams, HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, MultipartParams, HeaderOnly).ConfigureAwait(false); else if (Json != null) - return await Client.Send(Method, Endpoint, Json, HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Json, HeaderOnly).ConfigureAwait(false); else - return await Client.Send(Method, Endpoint, HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, HeaderOnly).ConfigureAwait(false); } } } diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs index 003bf731f..1a841b603 100644 --- a/src/Discord.Net/Net/Queue/WebSocketRequest.cs +++ b/src/Discord.Net/Net/Queue/WebSocketRequest.cs @@ -32,9 +32,9 @@ namespace Discord.Net.Queue Promise = new TaskCompletionSource(); } - public async Task Send() + public async Task SendAsync() { - await Client.Send(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); + await Client.SendAsync(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); return null; } } diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index f870cf61e..c51c5fa0a 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -67,22 +67,22 @@ namespace Discord.Net.Rest _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task Send(string method, string endpoint, bool headerOnly = false) + public async Task SendAsync(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); + return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); } - public async Task Send(string method, string endpoint, string json, bool headerOnly = false) + public async Task SendAsync(string method, string endpoint, string json, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); + return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); } } - public async Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) @@ -110,11 +110,11 @@ namespace Discord.Net.Rest } } restRequest.Content = content; - return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); + return await SendInternalAsync(restRequest, headerOnly).ConfigureAwait(false); } } - private async Task SendInternal(HttpRequestMessage request, bool headerOnly) + private async Task SendInternalAsync(HttpRequestMessage request, bool headerOnly) { while (true) { diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index 25b577688..57b5f91ca 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -11,8 +11,8 @@ namespace Discord.Net.Rest void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); - 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); + Task SendAsync(string method, string endpoint, bool headerOnly = false); + Task SendAsync(string method, string endpoint, string json, bool headerOnly = false); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false); } } diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 545a92d37..eea88605d 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -50,18 +50,18 @@ namespace Discord.Net.WebSockets Dispose(true); } - public async Task Connect(string host) + public async Task ConnectAsync(string host) { //Assume locked - await Disconnect().ConfigureAwait(false); + await DisconnectAsync().ConfigureAwait(false); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); - _task = Run(_cancelToken); + _task = RunAsync(_cancelToken); } - public async Task Disconnect() + public async Task DisconnectAsync() { //Assume locked _cancelTokenSource.Cancel(); @@ -82,7 +82,7 @@ namespace Discord.Net.WebSockets _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task Send(byte[] data, int index, int count, bool isText) + public async Task SendAsync(byte[] data, int index, int count, bool isText) { await _sendLock.WaitAsync(_cancelToken); try @@ -118,7 +118,7 @@ namespace Discord.Net.WebSockets } //TODO: Check this code - private async Task Run(CancellationToken cancelToken) + private async Task RunAsync(CancellationToken cancelToken) { var buffer = new ArraySegment(new byte[ReceiveChunkSize]); var stream = new MemoryStream(); @@ -151,11 +151,11 @@ namespace Discord.Net.WebSockets var array = stream.ToArray(); if (result.MessageType == WebSocketMessageType.Binary) - await BinaryMessage.Raise(array, 0, array.Length).ConfigureAwait(false); + await BinaryMessage.RaiseAsync(array, 0, array.Length).ConfigureAwait(false); else if (result.MessageType == WebSocketMessageType.Text) { string text = Encoding.UTF8.GetString(array, 0, array.Length); - await TextMessage.Raise(text).ConfigureAwait(false); + await TextMessage.RaiseAsync(text).ConfigureAwait(false); } stream.Position = 0; diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs index 2925c1350..583aaa06d 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -12,9 +12,9 @@ namespace Discord.Net.WebSockets void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); - Task Connect(string host); - Task Disconnect(); + Task ConnectAsync(string host); + Task DisconnectAsync(); - Task Send(byte[] data, int index, int count, bool isText); + Task SendAsync(byte[] data, int index, int count, bool isText); } } diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs index 991dde11f..6fd12aa4e 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -81,17 +81,17 @@ namespace Discord .ToImmutableArray(); } - public async Task Download(ulong id) + public async Task DownloadAsync(ulong id) { var msg = Get(id); if (msg != null) return msg; - var model = await _discord.ApiClient.GetChannelMessage(_channel.Id, id).ConfigureAwait(false); + var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); if (model != null) return new CachedMessage(_channel, new User(_discord, model.Author), model); return null; } - public async Task> Download(ulong? fromId, Direction dir, int limit) + public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) { //TODO: Test heavily, especially the ordering of messages if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); @@ -110,7 +110,7 @@ namespace Discord RelativeDirection = dir, RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id }; - var downloadedMessages = await _discord.ApiClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false); + var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetCachedUser(x.Id), x))).ToImmutableArray(); } } From dea73042aa190da8206b1049e02721c09f7369b3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 9 Jun 2016 02:30:05 -0300 Subject: [PATCH 018/160] Added missing logger method --- src/Discord.Net/Logging/Logger.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 36897ea44..2255f4451 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -16,6 +16,8 @@ namespace Discord.Logging Name = name; } + public Task LogAsync(LogSeverity severity, Exception exception = null) + => _manager.LogAsync(severity, Name, exception); public Task LogAsync(LogSeverity severity, string message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) From 59e6b33cbb0e0c7037f88df5978e0fc54ae0f30c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 9 Jun 2016 12:39:38 -0300 Subject: [PATCH 019/160] Removed some C#7 stuff --- .../Permissions/ChannelPermissions.cs | 19 ++----------------- .../Entities/Permissions/GuildPermissions.cs | 5 +---- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs index 5084d6ac6..39767179a 100644 --- a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -7,37 +7,22 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { -#if CSHARP7 - private static ChannelPermissions _allDM { get; } = new ChannelPermissions(0b000100_000000_0011111111_0000011001); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(0b000000_000000_0001110011_0000000000); - private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(0b000100_111111_0000000000_0000011001); -#else + //TODO: C#7 Candidate for binary literals private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000000111111110000011001", 2)); private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000000011100110000000000", 2)); private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000011001", 2)); -#endif /// Gets a blank ChannelPermissions that grants no permissions. public static ChannelPermissions None { get; } = new ChannelPermissions(); /// Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { -#if CSHARP7 - switch (channel) - { - case ITextChannel _: return _allText; - case IVoiceChannel _: return _allVoice; - case IDMChannel _: return _allDM; - default: - throw new ArgumentException("Unknown channel type", nameof(channel)); - } -#else + //TODO: C#7 Candidate for typeswitch if (channel is ITextChannel) return _allText; if (channel is IVoiceChannel) return _allVoice; if (channel is IDMChannel) return _allDM; throw new ArgumentException("Unknown channel type", nameof(channel)); -#endif } /// Gets a packed value representing all the permissions in this ChannelPermissions. diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs index 4240a6cc3..1f17aa31e 100644 --- a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -10,11 +10,8 @@ namespace Discord /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); /// Gets a GuildPermissions that grants all permissions. -#if CSHARP7 - public static readonly GuildPermissions All = new GuildPermissions(0b000111_111111_0011111111_0000111111); -#else + //TODO: C#7 Candidate for binary literals public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("00011111111100111111110000111111", 2)); -#endif /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } From fe8a7661330fbe4dba74f7b6397d7f6c5a436f45 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 9 Jun 2016 13:39:39 -0300 Subject: [PATCH 020/160] Added Available to IGuild --- src/Discord.Net/Entities/Guilds/Guild.cs | 1 + src/Discord.Net/Entities/Guilds/IGuild.cs | 4 +++- src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index c8e79238b..0778d3597 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -303,6 +303,7 @@ namespace Discord private string DebuggerDisplay => $"{Name} ({Id})"; + bool IGuild.Available => false; IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 9949b788d..3300132b0 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -18,6 +18,8 @@ namespace Discord string IconUrl { get; } /// Returns the url to this guild's splash image, or null if one is not set. string SplashUrl { get; } + /// Returns true if this guild is currently connected and ready to be used. Only applies to the WebSocket client. + bool Available { get; } /// Gets the id of the AFK voice channel for this guild if set, or null if not. ulong? AFKChannelId { get; } @@ -90,7 +92,7 @@ namespace Discord Task GetUserAsync(ulong id); /// Gets the current user for this guild. Task GetCurrentUserAsync(); - /// Downloads all users for this guild if the current list is incomplete. + /// Downloads all users for this guild if the current list is incomplete. Only applies to the WebSocket client. Task DownloadUsersAsync(); /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsersAsync(int days = 30, bool simulate = false); diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 058cd5b52..0e567fba1 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -24,7 +24,7 @@ namespace Discord private ConcurrentDictionary _presences; private ConcurrentDictionary _voiceStates; - public bool Available { get; private set; } //TODO: Add to IGuild + public bool Available { get; private set; } public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } @@ -102,7 +102,7 @@ namespace Discord if (model.VoiceStates != null) { for (int i = 0; i < model.VoiceStates.Length; i++) - AddOrUpdateCachedVoiceState(model.VoiceStates[i], _voiceStates); + AddOrUpdateCachedVoiceState(model.VoiceStates[i], voiceStates); } _voiceStates = voiceStates; } From a3b02220b7e1afe5e0ef5fc8df5114fa3f495f68 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 9 Jun 2016 13:40:02 -0300 Subject: [PATCH 021/160] Added all missing ConfigureAwait(false)s --- src/Discord.Net/DiscordSocketClient.cs | 75 +++++++++++-------- .../Net/WebSockets/DefaultWebsocketClient.cs | 14 +++- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index eeb51f52a..5a160e8c5 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -115,7 +115,7 @@ namespace Discord _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}"); + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; GatewaySocket = config.WebSocketProvider(); @@ -415,15 +415,26 @@ namespace Discord case "GUILD_CREATE": { var data = (payload as JToken).ToObject(_serializer); - var guild = new CachedGuild(this, data, DataStore); - DataStore.AddGuild(guild); if (data.Unavailable == false) type = "GUILD_AVAILABLE"; await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + CachedGuild guild; if (data.Unavailable != false) + { + guild = new CachedGuild(this, data, DataStore); + DataStore.AddGuild(guild); await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); + } + else + { + guild = DataStore.GetGuild(data.Id); + if (guild != null) + guild.Update(data, UpdateSource.WebSocket); + else + await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + } await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); } @@ -481,12 +492,12 @@ namespace Discord DataStore.AddChannel(channel); } else - await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); } else channel = AddCachedDMChannel(data); if (channel != null) - await ChannelCreated.RaiseAsync(channel); + await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } break; case "CHANNEL_UPDATE": @@ -499,10 +510,10 @@ namespace Discord { var before = _enablePreUpdateEvents ? channel.Clone() : null; channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.RaiseAsync(before, channel); + await ChannelUpdated.RaiseAsync(before, channel).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); } break; case "CHANNEL_DELETE": @@ -512,9 +523,9 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var channel = RemoveCachedChannel(data.Id); if (channel != null) - await ChannelDestroyed.RaiseAsync(channel); + await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); else - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); } break; @@ -531,7 +542,7 @@ namespace Discord await UserJoined.RaiseAsync(user).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_MEMBER_UPDATE": @@ -547,13 +558,13 @@ namespace Discord { var before = _enablePreUpdateEvents ? user.Clone() : null; user.Update(data, UpdateSource.WebSocket); - await UserUpdated.RaiseAsync(before, user); + await UserUpdated.RaiseAsync(before, user).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_MEMBER_REMOVE": @@ -568,13 +579,13 @@ namespace Discord if (user != null) { user.User.RemoveRef(); - await UserLeft.RaiseAsync(user); + await UserLeft.RaiseAsync(user).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_MEMBERS_CHUNK": @@ -595,7 +606,7 @@ namespace Discord } } else - await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); } break; @@ -612,7 +623,7 @@ namespace Discord await RoleCreated.RaiseAsync(role).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_ROLE_UPDATE": @@ -631,10 +642,10 @@ namespace Discord await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_ROLE_DELETE": @@ -649,10 +660,10 @@ namespace Discord if (role != null) await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); else - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); } break; @@ -664,9 +675,9 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserBanned.RaiseAsync(new User(this, data)); + await UserBanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); else - await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); } break; case "GUILD_BAN_REMOVE": @@ -676,9 +687,9 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserUnbanned.RaiseAsync(new User(this, data)); + await UserUnbanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); else - await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); } break; @@ -696,13 +707,13 @@ namespace Discord if (author != null) { var msg = channel.AddCachedMessage(author, data); - await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); + await MessageReceived.RaiseAsync(msg).ConfigureAwait(false).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user."); + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); } break; case "MESSAGE_UPDATE": @@ -719,7 +730,7 @@ namespace Discord await MessageUpdated.RaiseAsync(before, msg).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); } break; case "MESSAGE_DELETE": @@ -734,7 +745,7 @@ namespace Discord await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); } else - await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel."); + await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); } break; @@ -755,7 +766,7 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId.Value); if (guild == null) { - await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); break; } if (data.Status == UserStatus.Offline) diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index eea88605d..7408e1557 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -65,9 +65,17 @@ namespace Discord.Net.WebSockets { //Assume locked _cancelTokenSource.Cancel(); - + if (_client.State == WebSocketState.Open) - try { await _client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } catch { } + { + try + { + var task = _client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + if (task != null) + await task.ConfigureAwait(false); + } + catch { } + } await (_task ?? Task.CompletedTask).ConfigureAwait(false); } @@ -84,7 +92,7 @@ namespace Discord.Net.WebSockets public async Task SendAsync(byte[] data, int index, int count, bool isText) { - await _sendLock.WaitAsync(_cancelToken); + await _sendLock.WaitAsync(_cancelToken).ConfigureAwait(false); try { //TODO: If connection is temporarily down, retry? From 67082559939d7138129e7d12b8b8f2ca56ece68a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 17:08:36 -0300 Subject: [PATCH 022/160] Minor log/doc edits --- src/Discord.Net/DiscordClient.cs | 4 +- src/Discord.Net/DiscordSocketClient.cs | 66 +++++++++++++++----------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 1fbd03953..0a33d9f4c 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -28,10 +28,10 @@ namespace Discord public LoginState LoginState { get; private set; } public API.DiscordApiClient ApiClient { get; private set; } - /// Creates a new discord client using only the REST API. + /// Creates a new REST-only discord client. public DiscordClient() : this(new DiscordConfig()) { } - /// Creates a new discord client using only the REST API. + /// Creates a new REST-only discord client. public DiscordClient(DiscordConfig config) { _log = new LogManager(config.LogLevel); diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 5a160e8c5..091202b3b 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -88,10 +88,10 @@ namespace Discord } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); - /// Creates a new discord client using the REST and WebSocket APIs. + /// Creates a new REST/WebSocket discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } - /// Creates a new discord client using the REST and WebSocket APIs. + /// Creates a new REST/WebSocket discord client. public DiscordSocketClient(DiscordSocketConfig config) : base(config) { @@ -365,7 +365,7 @@ namespace Discord { case GatewayOpCode.Hello: { - await _gatewayLogger.DebugAsync($"Received Hello").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); await ApiClient.SendIdentifyAsync().ConfigureAwait(false); @@ -374,10 +374,10 @@ namespace Discord break; case GatewayOpCode.HeartbeatAck: { - await _gatewayLogger.DebugAsync($"Received HeartbeatAck").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Latency = {latency} ms").ConfigureAwait(false); Latency = latency; await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); @@ -389,7 +389,7 @@ namespace Discord //Global case "READY": { - await _gatewayLogger.DebugAsync($"Received Dispatch (READY)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); //TODO: Make downloading large guilds optional var data = (payload as JToken).ToObject(_serializer); @@ -441,7 +441,7 @@ namespace Discord break; case "GUILD_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.Id); @@ -479,7 +479,7 @@ namespace Discord //Channels case "CHANNEL_CREATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; @@ -502,7 +502,7 @@ namespace Discord break; case "CHANNEL_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.Id); @@ -518,7 +518,7 @@ namespace Discord break; case "CHANNEL_DELETE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = RemoveCachedChannel(data.Id); @@ -532,7 +532,7 @@ namespace Discord //Members case "GUILD_MEMBER_ADD": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -547,7 +547,7 @@ namespace Discord break; case "GUILD_MEMBER_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -569,7 +569,7 @@ namespace Discord break; case "GUILD_MEMBER_REMOVE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -590,7 +590,7 @@ namespace Discord break; case "GUILD_MEMBERS_CHUNK": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -613,7 +613,7 @@ namespace Discord //Roles case "GUILD_ROLE_CREATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -628,7 +628,7 @@ namespace Discord break; case "GUILD_ROLE_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -650,7 +650,7 @@ namespace Discord break; case "GUILD_ROLE_DELETE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -670,7 +670,7 @@ namespace Discord //Bans case "GUILD_BAN_ADD": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -682,7 +682,7 @@ namespace Discord break; case "GUILD_BAN_REMOVE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -696,7 +696,7 @@ namespace Discord //Messages case "MESSAGE_CREATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -707,7 +707,7 @@ namespace Discord if (author != null) { var msg = channel.AddCachedMessage(author, data); - await MessageReceived.RaiseAsync(msg).ConfigureAwait(false).ConfigureAwait(false); + await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); } else await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); @@ -718,7 +718,7 @@ namespace Discord break; case "MESSAGE_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -735,7 +735,7 @@ namespace Discord break; case "MESSAGE_DELETE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -752,7 +752,7 @@ namespace Discord //Statuses case "PRESENCE_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.GuildId == null) @@ -778,7 +778,7 @@ namespace Discord break; case "TYPING_START": { - await _gatewayLogger.DebugAsync($"Received Dispatch (TYPING_START)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; @@ -796,7 +796,7 @@ namespace Discord //Voice case "VOICE_STATE_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.GuildId.HasValue) @@ -821,7 +821,7 @@ namespace Discord //Settings case "USER_UPDATE": { - await _gatewayLogger.DebugAsync($"Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); if (data.Id == CurrentUser.Id) @@ -835,12 +835,22 @@ namespace Discord //Ignored case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + return; case "MESSAGE_ACK": //TODO: Add (User only) + await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); + return; case "GUILD_EMOJIS_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); + return; case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + return; case "VOICE_SERVER_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + return; case "RESUMED": //TODO: Add - await _gatewayLogger.DebugAsync($"Ignored Dispatch ({type})").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Ignored Dispatch (RESUMED)").ConfigureAwait(false); return; //Others From e76f1c64c41e7e04de9f13e44280f6f941192dd2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 17:53:55 -0300 Subject: [PATCH 023/160] Fixed several websocket cache issues --- src/Discord.Net/DiscordSocketClient.cs | 220 +++++++++++------- .../Entities/WebSocket/CachedDMChannel.cs | 12 +- .../Entities/WebSocket/CachedGuild.cs | 53 ++--- .../Entities/WebSocket/CachedGuildUser.cs | 4 +- .../Entities/WebSocket/CachedPublicUser.cs | 5 +- .../Entities/WebSocket/CachedTextChannel.cs | 16 +- .../Entities/WebSocket/CachedVoiceChannel.cs | 6 +- .../WebSocket/ICachedMessageChannel.cs | 8 +- src/Discord.Net/Utilities/MessageCache.cs | 2 +- 9 files changed, 189 insertions(+), 137 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 091202b3b..fb75993e5 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -209,30 +209,21 @@ namespace Discord { return Task.FromResult(DataStore.GetGuild(id)); } - internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null) + internal CachedGuild AddGuild(API.Gateway.ExtendedGuild model, DataStore dataStore) { - dataStore = dataStore ?? DataStore; - var guild = new CachedGuild(this, model, dataStore); - if (model.Unavailable != true) - { - for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(guild, model.Channels[i], dataStore); - } dataStore.AddGuild(guild); if (model.Large) _largeGuilds.Enqueue(model.Id); return guild; } - internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null) + internal CachedGuild RemoveGuild(ulong id) { - dataStore = dataStore ?? DataStore; - - var guild = dataStore.RemoveGuild(id); + var guild = DataStore.RemoveGuild(id); foreach (var channel in guild.Channels) - guild.RemoveCachedChannel(channel.Id); + guild.RemoveChannel(channel.Id); foreach (var user in guild.Members) - guild.RemoveCachedUser(user.Id); + guild.RemoveUser(user.Id); return guild; } @@ -241,46 +232,19 @@ namespace Discord { return Task.FromResult(DataStore.GetChannel(id)); } - internal ICachedGuildChannel AddCachedChannel(CachedGuild guild, API.Channel model, DataStore dataStore = null) + internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) { - dataStore = dataStore ?? DataStore; - - var channel = guild.AddCachedChannel(model); - dataStore.AddChannel(channel); - return channel; - } - internal CachedDMChannel AddCachedDMChannel(API.Channel model, DataStore dataStore = null) - { - dataStore = dataStore ?? DataStore; - - var recipient = GetOrAddCachedUser(model.Recipient, dataStore); + var recipient = GetOrAddUser(model.Recipient, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; } - internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null) - { - dataStore = dataStore ?? DataStore; - - //TODO: C#7 Typeswitch Candidate - var channel = DataStore.RemoveChannel(id); - - var guildChannel = channel as ICachedGuildChannel; - if (guildChannel != null) - { - guildChannel.Guild.RemoveCachedChannel(guildChannel.Id); - return channel; - } - - var dmChannel = channel as CachedDMChannel; - if (dmChannel != null) - { - var recipient = dmChannel.Recipient; - recipient.RemoveDMChannel(id); - return channel; - } - - return null; + internal CachedDMChannel RemoveDMChannel(ulong id) + { + var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; + var recipient = dmChannel.Recipient; + recipient.RemoveDMChannel(id); + return dmChannel; } /// @@ -293,20 +257,15 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } - internal CachedPublicUser GetOrAddCachedUser(API.User model, DataStore dataStore = null) + internal CachedPublicUser GetOrAddUser(API.User model, DataStore dataStore) { - dataStore = dataStore ?? DataStore; - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)); user.AddRef(); return user; } - internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null) + internal CachedPublicUser RemoveUser(ulong id) { - dataStore = dataStore ?? DataStore; - - var user = dataStore.RemoveUser(id); - return user; + return DataStore.RemoveUser(id); } /// Downloads the members list for all large guilds. @@ -395,14 +354,16 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - _currentUser = new CachedSelfUser(this, data.User); + var currentUser = new CachedSelfUser(this, data.User); + //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); for (int i = 0; i < data.Guilds.Length; i++) - AddCachedGuild(data.Guilds[i], dataStore); + AddGuild(data.Guilds[i], dataStore); for (int i = 0; i < data.PrivateChannels.Length; i++) - AddCachedDMChannel(data.PrivateChannels[i], dataStore); + AddDMChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; + _currentUser = currentUser; DataStore = dataStore; await Ready.RaiseAsync().ConfigureAwait(false); @@ -423,8 +384,7 @@ namespace Discord CachedGuild guild; if (data.Unavailable != false) { - guild = new CachedGuild(this, data, DataStore); - DataStore.AddGuild(guild); + guild = AddGuild(data, DataStore); await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); } else @@ -433,7 +393,10 @@ namespace Discord if (guild != null) guild.Update(data, UpdateSource.WebSocket); else + { await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + return; + } } await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); @@ -452,7 +415,10 @@ namespace Discord await GuildUpdated.RaiseAsync(before, guild).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild."); + return; + } } break; case "GUILD_DELETE": @@ -462,7 +428,7 @@ namespace Discord type = "GUILD_UNAVAILABLE"; await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - var guild = DataStore.RemoveGuild(data.Id); + var guild = RemoveGuild(data.Id); if (guild != null) { await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); @@ -472,7 +438,10 @@ namespace Discord member.User.RemoveRef(); } else + { await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; @@ -487,15 +456,15 @@ namespace Discord { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) - { - channel = guild.AddCachedChannel(data); - DataStore.AddChannel(channel); - } + guild.AddChannel(data, DataStore); else + { await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); + return; + } } else - channel = AddCachedDMChannel(data); + channel = AddDMChannel(data, DataStore); if (channel != null) await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } @@ -513,19 +482,38 @@ namespace Discord await ChannelUpdated.RaiseAsync(before, channel).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; case "CHANNEL_DELETE": { await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + ICachedChannel channel = null; var data = (payload as JToken).ToObject(_serializer); - var channel = RemoveCachedChannel(data.Id); + if (data.GuildId != null) + { + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) + channel = guild.RemoveChannel(data.Id); + else + { + await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + else + channel = RemoveDMChannel(data.Id); if (channel != null) await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); else + { await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; @@ -538,11 +526,14 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - var user = guild.AddCachedUser(data); + var user = guild.AddUser(data, DataStore); await UserJoined.RaiseAsync(user).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_MEMBER_UPDATE": @@ -553,7 +544,7 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - var user = guild.GetCachedUser(data.User.Id); + var user = guild.GetUser(data.User.Id); if (user != null) { var before = _enablePreUpdateEvents ? user.Clone() : null; @@ -561,10 +552,16 @@ namespace Discord await UserUpdated.RaiseAsync(before, user).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); + return; + } } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_MEMBER_REMOVE": @@ -575,17 +572,23 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - var user = guild.RemoveCachedUser(data.User.Id); + var user = guild.RemoveUser(data.User.Id); if (user != null) { user.User.RemoveRef(); await UserLeft.RaiseAsync(user).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); + return; + } } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_MEMBERS_CHUNK": @@ -597,7 +600,7 @@ namespace Discord if (guild != null) { foreach (var memberModel in data.Members) - guild.AddCachedUser(memberModel); + guild.AddUser(memberModel, DataStore); if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { @@ -606,7 +609,10 @@ namespace Discord } } else + { await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; @@ -619,11 +625,14 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - var role = guild.AddCachedRole(data.Role); + var role = guild.AddRole(data.Role); await RoleCreated.RaiseAsync(role).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_ROLE_UPDATE": @@ -642,10 +651,16 @@ namespace Discord await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); + return; + } } else + { await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_ROLE_DELETE": @@ -656,14 +671,20 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - var role = guild.RemoveCachedRole(data.RoleId); + var role = guild.RemoveRole(data.RoleId); if (role != null) await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); else + { await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); + return; + } } else + { await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; @@ -677,7 +698,10 @@ namespace Discord if (guild != null) await UserBanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); else + { await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; case "GUILD_BAN_REMOVE": @@ -689,7 +713,10 @@ namespace Discord if (guild != null) await UserUnbanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); else + { await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); + return; + } } break; @@ -702,18 +729,24 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var author = channel.GetCachedUser(data.Author.Id); + var author = channel.GetUser(data.Author.Id); if (author != null) { - var msg = channel.AddCachedMessage(author, data); + var msg = channel.AddMessage(author, data); await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); + return; + } } else + { await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; case "MESSAGE_UPDATE": @@ -724,13 +757,16 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var msg = channel.GetCachedMessage(data.Id); + var msg = channel.GetMessage(data.Id); var before = _enablePreUpdateEvents ? msg.Clone() : null; msg.Update(data, UpdateSource.WebSocket); await MessageUpdated.RaiseAsync(before, msg).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; case "MESSAGE_DELETE": @@ -741,11 +777,14 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var msg = channel.RemoveCachedMessage(data.Id); + var msg = channel.RemoveMessage(data.Id); await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; @@ -770,9 +809,9 @@ namespace Discord break; } if (data.Status == UserStatus.Offline) - guild.RemoveCachedPresence(data.User.Id); + guild.RemovePresence(data.User.Id); else - guild.AddOrUpdateCachedPresence(data); + guild.AddOrUpdatePresence(data); } } break; @@ -784,12 +823,15 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var user = channel.GetCachedUser(data.UserId); + var user = channel.GetUser(data.UserId); if (user != null) await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); } else + { await _gatewayLogger.WarningAsync("TYPING_START referenced an unknown channel.").ConfigureAwait(false); + return; + } } break; @@ -805,15 +847,18 @@ namespace Discord if (guild != null) { if (data.ChannelId == null) - guild.RemoveCachedVoiceState(data.UserId); + guild.RemoveVoiceState(data.UserId); else - guild.AddOrUpdateCachedVoiceState(data); + guild.AddOrUpdateVoiceState(data); - var user = guild.GetCachedUser(data.UserId); + var user = guild.GetUser(data.UserId); user.Update(data, UpdateSource.WebSocket); } else + { await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } } } break; @@ -830,6 +875,11 @@ namespace Discord CurrentUser.Update(data, UpdateSource.WebSocket); await CurrentUserUpdated.RaiseAsync(before, CurrentUser).ConfigureAwait(false); } + else + { + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; + } } break; diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 2bba02a46..7b211bf42 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -21,11 +21,11 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); public override Task> GetUsersAsync() => Task.FromResult>(Members); public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public ICachedUser GetCachedUser(ulong id) + public ICachedUser GetUser(ulong id) { var currentUser = Discord.CurrentUser; if (id == Recipient.Id) @@ -48,24 +48,24 @@ namespace Discord { return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) + public CachedMessage AddMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); return msg; } - public CachedMessage GetCachedMessage(ulong id) + public CachedMessage GetMessage(ulong id) { return _messages.Get(id); } - public CachedMessage RemoveCachedMessage(ulong id) + public CachedMessage RemoveMessage(ulong id) { return _messages.Remove(id); } public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 0e567fba1..f8e3c3da0 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -32,8 +32,8 @@ namespace Discord public Task DownloaderPromise => _downloaderPromise.Task; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); - public IReadOnlyCollection Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); + public CachedGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); + public IReadOnlyCollection Channels => _channels.Select(x => GetChannel(x)).ToReadOnlyCollection(_channels); public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) @@ -74,7 +74,7 @@ namespace Discord if (model.Channels != null) { for (int i = 0; i < model.Channels.Length; i++) - AddCachedChannel(model.Channels[i], channels); + AddChannel(model.Channels[i], dataStore, channels); } _channels = channels; @@ -82,7 +82,7 @@ namespace Discord if (model.Presences != null) { for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdateCachedPresence(model.Presences[i], presences); + AddOrUpdatePresence(model.Presences[i], presences); } _presences = presences; @@ -90,7 +90,7 @@ namespace Discord if (model.Members != null) { for (int i = 0; i < model.Members.Length; i++) - AddCachedUser(model.Members[i], members, dataStore); + AddUser(model.Members[i], dataStore, members); _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = model.Members.Length; if (!model.Large) @@ -102,43 +102,44 @@ namespace Discord if (model.VoiceStates != null) { for (int i = 0; i < model.VoiceStates.Length; i++) - AddOrUpdateCachedVoiceState(model.VoiceStates[i], voiceStates); + AddOrUpdateVoiceState(model.VoiceStates[i], voiceStates); } _voiceStates = voiceStates; } - public override Task GetChannelAsync(ulong id) => Task.FromResult(GetCachedChannel(id)); + public override Task GetChannelAsync(ulong id) => Task.FromResult(GetChannel(id)); public override Task> GetChannelsAsync() => Task.FromResult>(Channels); - public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet channels = null) + public void AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet channels = null) { var channel = ToChannel(model); (channels ?? _channels).TryAdd(model.Id); - return channel; + dataStore.AddChannel(channel); } - public ICachedGuildChannel GetCachedChannel(ulong id) + public ICachedGuildChannel GetChannel(ulong id) { return Discord.DataStore.GetChannel(id) as ICachedGuildChannel; } - public void RemoveCachedChannel(ulong id, ConcurrentHashSet channels = null) + public ICachedGuildChannel RemoveChannel(ulong id) { - (channels ?? _channels).TryRemove(id); + _channels.TryRemove(id); + return Discord.DataStore.RemoveChannel(id) as ICachedGuildChannel; } - public Presence AddOrUpdateCachedPresence(PresenceModel model, ConcurrentDictionary presences = null) + public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null) { var game = model.Game != null ? new Game(model.Game) : (Game?)null; var presence = new Presence(model.Status, game); (presences ?? _presences)[model.User.Id] = presence; return presence; } - public Presence? GetCachedPresence(ulong id) + public Presence? GetPresence(ulong id) { Presence presence; if (_presences.TryGetValue(id, out presence)) return presence; return null; } - public Presence? RemoveCachedPresence(ulong id) + public Presence? RemovePresence(ulong id) { Presence presence; if (_presences.TryRemove(id, out presence)) @@ -146,13 +147,13 @@ namespace Discord return null; } - public Role AddCachedRole(RoleModel model, ConcurrentDictionary roles = null) + public Role AddRole(RoleModel model, ConcurrentDictionary roles = null) { var role = new Role(this, model); (roles ?? _roles)[model.Id] = role; return role; } - public Role RemoveCachedRole(ulong id) + public Role RemoveRole(ulong id) { Role role; if (_roles.TryRemove(id, out role)) @@ -160,21 +161,21 @@ namespace Discord return null; } - public VoiceState AddOrUpdateCachedVoiceState(VoiceStateModel model, ConcurrentDictionary voiceStates = null) + public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, ConcurrentDictionary voiceStates = null) { - var voiceChannel = GetCachedChannel(model.ChannelId.Value) as CachedVoiceChannel; + var voiceChannel = GetChannel(model.ChannelId.Value) as CachedVoiceChannel; var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); (voiceStates ?? _voiceStates)[model.UserId] = voiceState; return voiceState; } - public VoiceState? GetCachedVoiceState(ulong id) + public VoiceState? GetVoiceState(ulong id) { VoiceState voiceState; if (_voiceStates.TryGetValue(id, out voiceState)) return voiceState; return null; } - public VoiceState? RemoveCachedVoiceState(ulong id) + public VoiceState? RemoveVoiceState(ulong id) { VoiceState voiceState; if (_voiceStates.TryRemove(id, out voiceState)) @@ -182,7 +183,7 @@ namespace Discord return null; } - public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); public override Task GetCurrentUserAsync() => Task.FromResult(CurrentUser); public override Task> GetUsersAsync() @@ -190,23 +191,23 @@ namespace Discord //TODO: Is there a better way of exposing pagination? public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); - public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary members = null, DataStore dataStore = null) + public CachedGuildUser AddUser(MemberModel model, DataStore dataStore, ConcurrentDictionary members = null) { - var user = Discord.GetOrAddCachedUser(model.User); + var user = Discord.GetOrAddUser(model.User, dataStore); var member = new CachedGuildUser(this, user, model); (members ?? _members)[user.Id] = member; user.AddRef(); DownloadedMemberCount++; return member; } - public CachedGuildUser GetCachedUser(ulong id) + public CachedGuildUser GetUser(ulong id) { CachedGuildUser member; if (_members.TryGetValue(id, out member)) return member; return null; } - public CachedGuildUser RemoveCachedUser(ulong id) + public CachedGuildUser RemoveUser(ulong id) { CachedGuildUser member; if (_members.TryRemove(id, out member)) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 8801d59d7..104e6f1da 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -8,11 +8,11 @@ namespace Discord public new CachedGuild Guild => base.Guild as CachedGuild; public new CachedPublicUser User => base.User as CachedPublicUser; - public Presence? Presence => Guild.GetCachedPresence(Id); + public Presence? Presence => Guild.GetPresence(Id); public override Game? Game => Presence?.Game; public override UserStatus Status => Presence?.Status ?? UserStatus.Offline; - public VoiceState? VoiceState => Guild.GetCachedVoiceState(Id); + public VoiceState? VoiceState => Guild.GetVoiceState(Id); public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index d1c67dac7..7a3f0663d 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -1,4 +1,5 @@ -using ChannelModel = Discord.API.Channel; +using Discord.Data; +using ChannelModel = Discord.API.Channel; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; @@ -64,7 +65,7 @@ namespace Discord lock (this) { if (--_references == 0 && DMChannel == null) - Discord.RemoveCachedUser(Id); + Discord.RemoveUser(Id); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index cedd5208b..ab13b82dc 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -23,13 +23,13 @@ namespace Discord _messages = new MessageCache(Discord, this); } - public override Task GetUserAsync(ulong id) => Task.FromResult(GetCachedUser(id)); + public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); public override Task> GetUsersAsync() => Task.FromResult>(Members); public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public CachedGuildUser GetCachedUser(ulong id) + public CachedGuildUser GetUser(ulong id) { - var user = Guild.GetCachedUser(id); + var user = Guild.GetUser(id); if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) return user; return null; @@ -48,17 +48,17 @@ namespace Discord return await _messages.DownloadAsync(fromMessageId, dir, limit).ConfigureAwait(false); } - public CachedMessage AddCachedMessage(ICachedUser author, MessageModel model) + public CachedMessage AddMessage(ICachedUser author, MessageModel model) { var msg = new CachedMessage(this, author, model); _messages.Add(msg); return msg; } - public CachedMessage GetCachedMessage(ulong id) + public CachedMessage GetMessage(ulong id) { return _messages.Get(id); } - public CachedMessage RemoveCachedMessage(ulong id) + public CachedMessage RemoveMessage(ulong id) { return _messages.Remove(id); } @@ -67,8 +67,8 @@ namespace Discord IReadOnlyCollection ICachedMessageChannel.Members => Members; - IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); - ICachedUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); + IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); + ICachedUser ICachedMessageChannel.GetUser(ulong id) => GetUser(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index 22639eb16..a567ba2ad 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -20,14 +20,14 @@ namespace Discord } public override Task GetUserAsync(ulong id) - => Task.FromResult(GetCachedUser(id)); + => Task.FromResult(GetUser(id)); public override Task> GetUsersAsync() => Task.FromResult(Members); public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); - public IGuildUser GetCachedUser(ulong id) + public IGuildUser GetUser(ulong id) { - var user = Guild.GetCachedUser(id); + var user = Guild.GetUser(id); if (user != null && user.VoiceChannel.Id == Id) return user; return null; diff --git a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs index 30ca49022..06cfd76fd 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs @@ -7,10 +7,10 @@ namespace Discord { IReadOnlyCollection Members { get; } - CachedMessage AddCachedMessage(ICachedUser author, MessageModel model); - new CachedMessage GetCachedMessage(ulong id); - CachedMessage RemoveCachedMessage(ulong id); + CachedMessage AddMessage(ICachedUser author, MessageModel model); + CachedMessage GetMessage(ulong id); + CachedMessage RemoveMessage(ulong id); - ICachedUser GetCachedUser(ulong id); + ICachedUser GetUser(ulong id); } } diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs index 6fd12aa4e..97bc6e813 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -111,7 +111,7 @@ namespace Discord RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id }; var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetCachedUser(x.Id), x))).ToImmutableArray(); + return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetUser(x.Id), x))).ToImmutableArray(); } } } From d05bdd7317f23cdb319a0bf615d56f3fbb1968be Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 18:04:05 -0300 Subject: [PATCH 024/160] Fixed ignoring GUILD_AVAILABLE event --- src/Discord.Net/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index fb75993e5..cc0630922 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -391,7 +391,7 @@ namespace Discord { guild = DataStore.GetGuild(data.Id); if (guild != null) - guild.Update(data, UpdateSource.WebSocket); + guild.Update(data, UpdateSource.WebSocket, DataStore); else { await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); From c1710338ba22945154964c62804ca0a9693c97af Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 18:05:40 -0300 Subject: [PATCH 025/160] Fixed null GuildUser.User --- src/Discord.Net/Entities/Users/GuildUser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index b520d301f..6a9b85da6 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -37,6 +37,7 @@ namespace Discord public GuildUser(Guild guild, User user, Model model) { Guild = guild; + User = user; Update(model, UpdateSource.Creation); } From 4b64807465fa21922da00d02d6d33a29560c0550 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 18:08:20 -0300 Subject: [PATCH 026/160] Added perf TODO --- src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index ab13b82dc..b997ead6d 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -29,6 +29,7 @@ namespace Discord => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser GetUser(ulong id) { + //TODO: It's slow to do a perms check here... Maybe only do it on external calls? var user = Guild.GetUser(id); if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) return user; From e01a0a3155ff5fdf8725ca107e3af4f086250ad8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 18:08:31 -0300 Subject: [PATCH 027/160] Fixed null permission overwrites --- src/Discord.Net/Entities/Channels/TextChannel.cs | 2 +- src/Discord.Net/Entities/Channels/VoiceChannel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 778eea0ac..9c55d77bd 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -27,7 +27,7 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; Topic = model.Topic; - base.Update(model, UpdateSource.Rest); + base.Update(model, source); } public async Task ModifyAsync(Action func) diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index 2d2d5ed63..dd6653942 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -21,7 +21,7 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - base.Update(model, UpdateSource.Rest); + base.Update(model, source); Bitrate = model.Bitrate; UserLimit = model.UserLimit; } From 06fd96a72c745b8d67a8cdc1cdadbf371f151c37 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 18:14:09 -0300 Subject: [PATCH 028/160] Fixed missing project reference --- Discord.Net.sln | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 520566a3d..804e73da8 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.25123.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90}.Debug|Any CPU.Build.0 = Debug|Any CPU - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90}.Release|Any CPU.ActiveCfg = Release|Any CPU - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90}.Release|Any CPU.Build.0 = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From cc5e0bbe136241e885091207a6526ec3611bd3f8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 19:00:33 -0300 Subject: [PATCH 029/160] Fixed several message parsing issues, added optional deserialization --- src/Discord.Net/API/Common/Embed.cs | 4 +- src/Discord.Net/API/Common/Message.cs | 20 ++-- src/Discord.Net/DiscordSocketClient.cs | 2 +- .../Entities/Channels/DMChannel.cs | 12 +- .../Entities/Channels/TextChannel.cs | 12 +- src/Discord.Net/Entities/Messages/Embed.cs | 6 +- src/Discord.Net/Entities/Messages/Message.cs | 107 +++++++++++------- .../Net/Converters/DiscordContractResolver.cs | 6 +- .../Net/Converters/OptionalConverter.cs | 10 +- src/Discord.Net/Utilities/MentionUtils.cs | 2 +- src/Discord.Net/Utilities/MessageCache.cs | 2 +- 11 files changed, 105 insertions(+), 78 deletions(-) diff --git a/src/Discord.Net/API/Common/Embed.cs b/src/Discord.Net/API/Common/Embed.cs index 5c732a9d1..394b460dd 100644 --- a/src/Discord.Net/API/Common/Embed.cs +++ b/src/Discord.Net/API/Common/Embed.cs @@ -13,8 +13,8 @@ namespace Discord.API [JsonProperty("url")] public string Url { get; set; } [JsonProperty("thumbnail")] - public EmbedThumbnail Thumbnail { get; set; } + public Optional Thumbnail { get; set; } [JsonProperty("provider")] - public EmbedProvider Provider { get; set; } + public Optional Provider { get; set; } } } diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index f2ef47be3..52950caf2 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -10,24 +10,22 @@ namespace Discord.API [JsonProperty("channel_id")] public ulong ChannelId { get; set; } [JsonProperty("author")] - public User Author { get; set; } + public Optional Author { get; set; } [JsonProperty("content")] - public string Content { get; set; } + public Optional Content { get; set; } [JsonProperty("timestamp")] - public DateTime Timestamp { get; set; } + public Optional Timestamp { get; set; } [JsonProperty("edited_timestamp")] - public DateTime? EditedTimestamp { get; set; } + public Optional EditedTimestamp { get; set; } [JsonProperty("tts")] - public bool IsTextToSpeech { get; set; } + public Optional IsTextToSpeech { get; set; } [JsonProperty("mention_everyone")] - public bool IsMentioningEveryone { get; set; } + public Optional IsMentioningEveryone { get; set; } [JsonProperty("mentions")] - public User[] Mentions { get; set; } + public Optional Mentions { get; set; } [JsonProperty("attachments")] - public Attachment[] Attachments { get; set; } + public Optional Attachments { get; set; } [JsonProperty("embeds")] - public Embed[] Embeds { get; set; } - /*[JsonProperty("nonce")] - public object Nonce { get; set; }*/ + public Optional Embeds { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index cc0630922..9e2e33146 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -729,7 +729,7 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var author = channel.GetUser(data.Author.Id); + var author = channel.GetUser(data.Author.Value.Id); if (author != null) { diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index b1df139f0..ca9d1c1f3 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -70,7 +70,7 @@ namespace Discord { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } public async Task SendFileAsync(string filePath, string text, bool isTTS) { @@ -79,33 +79,33 @@ namespace Discord { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } } public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } public virtual async Task GetMessageAsync(ulong id) { var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); return null; } public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); } public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); } public async Task DeleteMessagesAsync(IEnumerable messages) { diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 9c55d77bd..78ec865fe 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -62,7 +62,7 @@ namespace Discord { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.CreateMessageAsync(Guild.Id, Id, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } public async Task SendFileAsync(string filePath, string text, bool isTTS) { @@ -71,33 +71,33 @@ namespace Discord { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, file, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } } public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, stream, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); } public virtual async Task GetMessageAsync(ulong id) { var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) - return new Message(this, new User(Discord, model.Author), model); + return new Message(this, new User(Discord, model.Author.Value), model); return null; } public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); } public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); } public async Task DeleteMessagesAsync(IEnumerable messages) { diff --git a/src/Discord.Net/Entities/Messages/Embed.cs b/src/Discord.Net/Entities/Messages/Embed.cs index 4e3d065a1..b27caeac2 100644 --- a/src/Discord.Net/Entities/Messages/Embed.cs +++ b/src/Discord.Net/Entities/Messages/Embed.cs @@ -18,8 +18,10 @@ namespace Discord Title = model.Title; Description = model.Description; - Provider = new EmbedProvider(model.Provider); - Thumbnail = new EmbedThumbnail(model.Thumbnail); + if (model.Provider.IsSpecified) + Provider = new EmbedProvider(model.Provider.Value); + if (model.Thumbnail.IsSpecified) + Thumbnail = new EmbedThumbnail(model.Thumbnail.Value); } } } diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index e72f89a57..c9f66bfe0 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -10,7 +10,9 @@ namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class Message : SnowflakeEntity, IMessage - { + { + private bool _isMentioningEveryone; + public DateTime? EditedTimestamp { get; private set; } public bool IsTTS { get; private set; } public string RawText { get; private set; } @@ -34,6 +36,13 @@ namespace Discord Channel = channel; Author = author; + if (channel is IGuildChannel) + { + MentionedUsers = ImmutableArray.Create(); + MentionedChannelIds = ImmutableArray.Create(); + MentionedRoleIds = ImmutableArray.Create(); + } + Update(model, UpdateSource.Creation); } public void Update(Model model, UpdateSource source) @@ -44,57 +53,73 @@ namespace Discord var guild = guildChannel?.Guild; var discord = Discord; - IsTTS = model.IsTextToSpeech; - Timestamp = model.Timestamp; - EditedTimestamp = model.EditedTimestamp; - RawText = model.Content; - - if (model.Attachments.Length > 0) + if (model.IsTextToSpeech.IsSpecified) + IsTTS = model.IsTextToSpeech.Value; + if (model.Timestamp.IsSpecified) + Timestamp = model.Timestamp.Value; + if (model.EditedTimestamp.IsSpecified) + EditedTimestamp = model.EditedTimestamp.Value; + if (model.IsMentioningEveryone.IsSpecified) + _isMentioningEveryone = model.IsMentioningEveryone.Value; + + if (model.Attachments.IsSpecified) { - var attachments = new Attachment[model.Attachments.Length]; - for (int i = 0; i < attachments.Length; i++) - attachments[i] = new Attachment(model.Attachments[i]); - Attachments = ImmutableArray.Create(attachments); + var value = model.Attachments.Value; + if (value.Length > 0) + { + var attachments = new Attachment[value.Length]; + for (int i = 0; i < attachments.Length; i++) + attachments[i] = new Attachment(value[i]); + Attachments = ImmutableArray.Create(attachments); + } + else + Attachments = ImmutableArray.Create(); } - else - Attachments = ImmutableArray.Create(); - if (model.Embeds.Length > 0) + if (model.Embeds.IsSpecified) { - var embeds = new Embed[model.Attachments.Length]; - for (int i = 0; i < embeds.Length; i++) - embeds[i] = new Embed(model.Embeds[i]); - Embeds = ImmutableArray.Create(embeds); + var value = model.Embeds.Value; + if (value.Length > 0) + { + var embeds = new Embed[value.Length]; + for (int i = 0; i < embeds.Length; i++) + embeds[i] = new Embed(value[i]); + Embeds = ImmutableArray.Create(embeds); + } + else + Embeds = ImmutableArray.Create(); } - else - Embeds = ImmutableArray.Create(); - if (guildChannel != null && model.Mentions.Length > 0) + if (model.Mentions.IsSpecified) { - var mentions = new User[model.Mentions.Length]; - for (int i = 0; i < model.Mentions.Length; i++) - mentions[i] = new User(discord, model.Mentions[i]); - MentionedUsers = ImmutableArray.Create(mentions); + var value = model.Mentions.Value; + if (value.Length > 0) + { + var mentions = new User[value.Length]; + for (int i = 0; i < value.Length; i++) + mentions[i] = new User(discord, value[i]); + MentionedUsers = ImmutableArray.Create(mentions); + } + else + MentionedUsers = ImmutableArray.Create(); } - else - MentionedUsers = ImmutableArray.Create(); - - if (guildChannel != null) - { - MentionedChannelIds = MentionUtils.GetChannelMentions(model.Content); - var mentionedRoleIds = MentionUtils.GetRoleMentions(model.Content); - if (model.IsMentioningEveryone) - mentionedRoleIds = mentionedRoleIds.Add(guildChannel.Guild.EveryoneRole.Id); - MentionedRoleIds = mentionedRoleIds; - } - else + if (model.Content.IsSpecified) { - MentionedChannelIds = ImmutableArray.Create(); - MentionedRoleIds = ImmutableArray.Create(); + RawText = model.Content.Value; + + if (Channel is IGuildChannel) + { + Text = MentionUtils.CleanUserMentions(RawText, MentionedUsers); + MentionedChannelIds = MentionUtils.GetChannelMentions(RawText); + var mentionedRoleIds = MentionUtils.GetRoleMentions(RawText); + if (_isMentioningEveryone) + mentionedRoleIds = mentionedRoleIds.Add(guildChannel.Guild.EveryoneRole.Id); + MentionedRoleIds = mentionedRoleIds; + } + else + Text = RawText; } - - Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); } public async Task UpdateAsync() diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 0149d130e..e240c2017 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -55,6 +55,7 @@ namespace Discord.Net.Converters converter = ImageConverter.Instance; else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) { + var innerType = type.GenericTypeArguments[0]; var typeInput = propInfo.DeclaringType; var typeOutput = propInfo.PropertyType; @@ -62,9 +63,10 @@ namespace Discord.Net.Converters var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, typeOutput); var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); - property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); - converter = OptionalConverter.Instance; + + var converterType = typeof(OptionalConverter<>).MakeGenericType(innerType); + converter = converterType.GetTypeInfo().GetDeclaredField("Instance").GetValue(null) as JsonConverter; } } diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs index acc583c5f..a1e7c7543 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -3,22 +3,22 @@ using System; namespace Discord.Net.Converters { - public class OptionalConverter : JsonConverter + public class OptionalConverter : JsonConverter { - public static readonly OptionalConverter Instance = new OptionalConverter(); + public static readonly OptionalConverter Instance = new OptionalConverter(); public override bool CanConvert(Type objectType) => true; - public override bool CanRead => false; + public override bool CanRead => true; public override bool CanWrite => true; public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - throw new InvalidOperationException(); + return new Optional(serializer.Deserialize(reader)); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - serializer.Serialize(writer, (value as IOptional).Value); + serializer.Serialize(writer, ((Optional)value).Value); } } } diff --git a/src/Discord.Net/Utilities/MentionUtils.cs b/src/Discord.Net/Utilities/MentionUtils.cs index 636711552..0053792c2 100644 --- a/src/Discord.Net/Utilities/MentionUtils.cs +++ b/src/Discord.Net/Utilities/MentionUtils.cs @@ -82,7 +82,7 @@ namespace Discord return builder; } - internal static string CleanUserMentions(string text, API.User[] mentions) + internal static string CleanUserMentions(string text, ImmutableArray mentions) { return _userRegex.Replace(text, new MatchEvaluator(e => { diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Utilities/MessageCache.cs index 97bc6e813..c0ddf5afd 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Utilities/MessageCache.cs @@ -88,7 +88,7 @@ namespace Discord return msg; var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); if (model != null) - return new CachedMessage(_channel, new User(_discord, model.Author), model); + return new CachedMessage(_channel, new User(_discord, model.Author.Value), model); return null; } public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) From 5278d798fafaf898492ccb141986023a472133dc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 19:01:39 -0300 Subject: [PATCH 030/160] Fixed latency log --- src/Discord.Net/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 9e2e33146..5694b72fc 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -336,7 +336,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.DebugAsync("Latency = {latency} ms").ConfigureAwait(false); + await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); Latency = latency; await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); From b15853dc8b278f5016be1fb90ada9bad4157c04e Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 11 Jun 2016 19:55:38 -0300 Subject: [PATCH 031/160] Several GuildUser fixes --- src/Discord.Net/API/Common/GuildMember.cs | 10 +++---- src/Discord.Net/Entities/Users/GuildUser.cs | 32 ++++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index d54f8b3a6..fb0157a90 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -8,14 +8,14 @@ namespace Discord.API [JsonProperty("user")] public User User { get; set; } [JsonProperty("nick")] - public string Nick { get; set; } + public Optional Nick { get; set; } [JsonProperty("roles")] - public ulong[] Roles { get; set; } + public Optional Roles { get; set; } [JsonProperty("joined_at")] - public DateTime?JoinedAt { get; set; } + public Optional JoinedAt { get; set; } [JsonProperty("deaf")] - public bool? Deaf { get; set; } + public Optional Deaf { get; set; } [JsonProperty("mute")] - public bool? Mute { get; set; } + public Optional Mute { get; set; } } } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 6a9b85da6..0eea42a39 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; @@ -9,6 +10,7 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { + [DebuggerDisplay("{DebuggerDisplay,nq}")] internal class GuildUser : IGuildUser, ISnowflakeEntity { public bool IsDeaf { get; private set; } @@ -45,20 +47,25 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - if (model.Deaf.HasValue) + if (model.Deaf.IsSpecified) IsDeaf = model.Deaf.Value; - if (model.Mute.HasValue) + if (model.Mute.IsSpecified) IsMute = model.Mute.Value; - JoinedAt = model.JoinedAt.Value; - Nickname = model.Nick; + if (model.JoinedAt.IsSpecified) + JoinedAt = model.JoinedAt.Value; + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; - var roles = ImmutableArray.CreateBuilder(model.Roles.Length + 1); - roles.Add(Guild.EveryoneRole); - for (int i = 0; i < model.Roles.Length; i++) - roles.Add(Guild.GetRole(model.Roles[i])); - Roles = roles.ToImmutable(); - - GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); + if (model.Roles.IsSpecified) + { + var value = model.Roles.Value; + var roles = ImmutableArray.CreateBuilder(value.Length + 1); + roles.Add(Guild.EveryoneRole); + for (int i = 0; i < value.Length; i++) + roles.Add(Guild.GetRole(value[i])); + Roles = roles.ToImmutable(); + GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); + } } public void Update(VoiceStateModel model, UpdateSource source) { @@ -108,6 +115,9 @@ namespace Discord await Discord.ApiClient.RemoveGuildMemberAsync(Guild.Id, Id).ConfigureAwait(false); } + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; + public ChannelPermissions GetPermissions(IGuildChannel channel) { if (channel == null) throw new ArgumentNullException(nameof(channel)); From c072666db54d076e4a288f48fea74fa758ca11d1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 00:55:11 -0300 Subject: [PATCH 032/160] Added GetAFK/Default/Embed/Owner extension methods to IGuild --- src/Discord.Net/Extensions/GuildExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Discord.Net/Extensions/GuildExtensions.cs b/src/Discord.Net/Extensions/GuildExtensions.cs index 8100ca4b5..e8895f22c 100644 --- a/src/Discord.Net/Extensions/GuildExtensions.cs +++ b/src/Discord.Net/Extensions/GuildExtensions.cs @@ -8,5 +8,24 @@ namespace Discord.Extensions => await guild.GetChannelAsync(id).ConfigureAwait(false) as ITextChannel; public static async Task GetVoiceChannelAsync(this IGuild guild, ulong id) => await guild.GetChannelAsync(id).ConfigureAwait(false) as IVoiceChannel; + + public static async Task GetAFKChannelAsync(this IGuild guild) + { + var afkId = guild.AFKChannelId; + if (afkId.HasValue) + return await guild.GetChannelAsync(afkId.Value).ConfigureAwait(false) as IVoiceChannel; + return null; + } + public static async Task GetDefaultChannelAsync(this IGuild guild) + => await guild.GetChannelAsync(guild.DefaultChannelId).ConfigureAwait(false) as ITextChannel; + public static async Task GetEmbedChannelAsync(this IGuild guild) + { + var embedId = guild.EmbedChannelId; + if (embedId.HasValue) + return await guild.GetChannelAsync(embedId.Value).ConfigureAwait(false) as IVoiceChannel; + return null; + } + public static async Task GetOwnerAsync(this IGuild guild) + => await guild.GetUserAsync(guild.OwnerId).ConfigureAwait(false); } } From 118d897c31215b5be783cb6f49220077ab225cd0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 01:12:09 -0300 Subject: [PATCH 033/160] Added CreatedAt to IInviteMetadata --- src/Discord.Net/Entities/Invites/IInviteMetadata.cs | 6 +++++- src/Discord.Net/Entities/Invites/InviteMetadata.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs index a2e18a2e7..45b936b22 100644 --- a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs @@ -1,4 +1,6 @@ -namespace Discord +using System; + +namespace Discord { public interface IInviteMetadata : IInvite { @@ -12,5 +14,7 @@ int? MaxUses { get; } /// Gets the amount of times this invite has been used. int Uses { get; } + /// Gets when this invite was created. + DateTime CreatedAt { get; } } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index 8f3ad5a64..1661f45d5 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -1,4 +1,5 @@ -using Model = Discord.API.InviteMetadata; +using System; +using Model = Discord.API.InviteMetadata; namespace Discord { @@ -9,6 +10,7 @@ namespace Discord public int? MaxAge { get; private set; } public int? MaxUses { get; private set; } public int Uses { get; private set; } + public DateTime CreatedAt { get; private set; } public InviteMetadata(DiscordClient client, Model model) : base(client, model) @@ -24,6 +26,7 @@ namespace Discord MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; MaxUses = model.MaxUses; Uses = model.Uses; + CreatedAt = model.CreatedAt; } } } From 7614b1921bed9ed0a9e578a81876e52944cf08e8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 02:38:32 -0300 Subject: [PATCH 034/160] Removed AssemblyInfo --- src/Discord.Net/Properties/AssemblyInfo.cs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/Discord.Net/Properties/AssemblyInfo.cs diff --git a/src/Discord.Net/Properties/AssemblyInfo.cs b/src/Discord.Net/Properties/AssemblyInfo.cs deleted file mode 100644 index 7dcbdb315..000000000 --- a/src/Discord.Net/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Discord.Net.Core")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("91e9e7bd-75c9-4e98-84aa-2c271922e5c2")] From f7c4371d9dcbcf1f0e81b514782210ee8dc34d7a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 02:39:49 -0300 Subject: [PATCH 035/160] Added explicit dependencies --- src/Discord.Net/project.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 4da1b01d3..bf7c6eb50 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -20,11 +20,17 @@ }, "dependencies": { - "NETStandard.Library": "1.5.0-rc2-24027", + "Microsoft.Win32.Primitives": "4.0.1-rc2-24027", "Newtonsoft.Json": "8.0.3", + "System.Collections.Concurrent": "4.0.12-rc2-24027", "System.Collections.Immutable": "1.2.0-rc2-24027", + "System.IO.Compression": "4.1.0-rc2-24027", + "System.IO.FileSystem": "4.0.1-rc2-24027", + "System.Net.Http": "4.0.1-rc2-24027", "System.Net.Websockets.Client": "4.0.0-rc2-24027", - "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027" + "System.Reflection.Extensions": "4.0.1-rc2-24027", + "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027", + "System.Text.RegularExpressions": "4.0.12-rc2-24027" }, "frameworks": { From 436274d408b1222a378055c350c1ccce9abe8614 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 02:40:13 -0300 Subject: [PATCH 036/160] Typo --- src/Discord.Net/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index bf7c6eb50..c2cb3b6c8 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -27,7 +27,7 @@ "System.IO.Compression": "4.1.0-rc2-24027", "System.IO.FileSystem": "4.0.1-rc2-24027", "System.Net.Http": "4.0.1-rc2-24027", - "System.Net.Websockets.Client": "4.0.0-rc2-24027", + "System.Net.WebSockets.Client": "4.0.0-rc2-24027", "System.Reflection.Extensions": "4.0.1-rc2-24027", "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027", "System.Text.RegularExpressions": "4.0.12-rc2-24027" From 15d7fcf46d0bbf1c50a208c02085b205c2cbf29f Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 12:23:17 -0300 Subject: [PATCH 037/160] Fixed crash when a user leaves the guild while in voice channel --- src/Discord.Net/DiscordSocketClient.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 5694b72fc..c08ce6f89 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -852,7 +852,13 @@ namespace Discord guild.AddOrUpdateVoiceState(data); var user = guild.GetUser(data.UserId); - user.Update(data, UpdateSource.WebSocket); + if (user != null) + user.Update(data, UpdateSource.WebSocket); + /*else //Happens when a user leaves/is kicked from a guild while in a voice channel + { + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); + return; + }*/ } else { From f3f4ba7d6607224ca4f5a2e1586367362446065d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 13:50:36 -0300 Subject: [PATCH 038/160] Fixed handling of invalid roles --- src/Discord.Net/DiscordSocketClient.cs | 5 +++++ src/Discord.Net/Entities/Users/GuildUser.cs | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index c08ce6f89..d2e3fa6cc 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -114,6 +114,11 @@ namespace Discord #endif _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _gatewayLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 0eea42a39..0ea00639d 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -62,7 +62,11 @@ namespace Discord var roles = ImmutableArray.CreateBuilder(value.Length + 1); roles.Add(Guild.EveryoneRole); for (int i = 0; i < value.Length; i++) - roles.Add(Guild.GetRole(value[i])); + { + var role = Guild.GetRole(value[i]); + if (role != null) + roles.Add(role); + } Roles = roles.ToImmutable(); GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } From cd3be8b5cf3c963bc38e8c792063d6c43a9a93dc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 14:02:02 -0300 Subject: [PATCH 039/160] Fixed handling of message updates when not in cache --- src/Discord.Net/DiscordSocketClient.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index d2e3fa6cc..509139ca7 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -762,10 +762,23 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var msg = channel.GetMessage(data.Id); - var before = _enablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data, UpdateSource.WebSocket); - await MessageUpdated.RaiseAsync(before, msg).ConfigureAwait(false); + IMessage before = null, after = null; + CachedMessage cachedMsg = channel.GetMessage(data.Id); + if (cachedMsg != null) + { + before = _enablePreUpdateEvents ? cachedMsg.Clone() : null; + cachedMsg.Update(data, UpdateSource.WebSocket); + after = cachedMsg; + } + else if (data.Author.IsSpecified) + { + //Edited message isnt in cache, create a detached one + var author = channel.GetUser(data.Author.Value.Id); + if (author != null) + after = new Message(channel, author, data); + } + if (after != null) + await MessageUpdated.RaiseAsync(before, after).ConfigureAwait(false); } else { From 0dfedabf4dea54b42aeb7af4401cd18bf62e2cd0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 16:08:43 -0300 Subject: [PATCH 040/160] Sync'd several entities to the API docs --- src/Discord.Net/API/Common/Attachment.cs | 4 +- src/Discord.Net/API/Common/Channel.cs | 18 +++---- src/Discord.Net/API/Common/EmbedThumbnail.cs | 4 +- src/Discord.Net/API/Common/Game.cs | 4 +- src/Discord.Net/API/Common/GuildEmbed.cs | 2 +- src/Discord.Net/API/Common/GuildMember.cs | 8 ++-- src/Discord.Net/API/Common/Presence.cs | 4 +- src/Discord.Net/API/Common/ReadState.cs | 2 +- src/Discord.Net/API/Common/Role.cs | 10 ++-- src/Discord.Net/API/DiscordAPIClient.cs | 2 +- src/Discord.Net/DiscordClient.cs | 6 +-- src/Discord.Net/DiscordSocketClient.cs | 20 ++++---- .../Entities/Channels/DMChannel.cs | 2 +- .../Entities/Channels/GuildChannel.cs | 12 ++--- .../Entities/Channels/TextChannel.cs | 2 +- .../Entities/Channels/VoiceChannel.cs | 4 +- src/Discord.Net/Entities/Guilds/Guild.cs | 4 +- .../Entities/Invites/IInviteMetadata.cs | 2 + .../Entities/Invites/InviteMetadata.cs | 2 + src/Discord.Net/Entities/Roles/Role.cs | 10 ++-- src/Discord.Net/Entities/Users/Game.cs | 2 +- src/Discord.Net/Entities/Users/GuildUser.cs | 47 +++++++++++-------- .../Entities/WebSocket/CachedGuild.cs | 16 +++---- src/Discord.Net/project.json | 1 - 24 files changed, 100 insertions(+), 88 deletions(-) diff --git a/src/Discord.Net/API/Common/Attachment.cs b/src/Discord.Net/API/Common/Attachment.cs index 1f2c4b8b7..e1561eb84 100644 --- a/src/Discord.Net/API/Common/Attachment.cs +++ b/src/Discord.Net/API/Common/Attachment.cs @@ -15,8 +15,8 @@ namespace Discord.API [JsonProperty("proxy_url")] public string ProxyUrl { get; set; } [JsonProperty("height")] - public int? Height { get; set; } + public Optional Height { get; set; } [JsonProperty("width")] - public int? Width { get; set; } + public Optional Width { get; set; } } } diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs index f50c31a09..b0789b111 100644 --- a/src/Discord.Net/API/Common/Channel.cs +++ b/src/Discord.Net/API/Common/Channel.cs @@ -14,28 +14,28 @@ namespace Discord.API //GuildChannel [JsonProperty("guild_id")] - public ulong? GuildId { get; set; } + public Optional GuildId { get; set; } [JsonProperty("name")] - public string Name { get; set; } + public Optional Name { get; set; } [JsonProperty("type")] - public ChannelType Type { get; set; } + public Optional Type { get; set; } [JsonProperty("position")] - public int Position { get; set; } + public Optional Position { get; set; } [JsonProperty("permission_overwrites")] - public Overwrite[] PermissionOverwrites { get; set; } + public Optional PermissionOverwrites { get; set; } //TextChannel [JsonProperty("topic")] - public string Topic { get; set; } + public Optional Topic { get; set; } //VoiceChannel [JsonProperty("bitrate")] - public int Bitrate { get; set; } + public Optional Bitrate { get; set; } [JsonProperty("user_limit")] - public int UserLimit { get; set; } + public Optional UserLimit { get; set; } //DMChannel [JsonProperty("recipient")] - public User Recipient { get; set; } + public Optional Recipient { get; set; } } } diff --git a/src/Discord.Net/API/Common/EmbedThumbnail.cs b/src/Discord.Net/API/Common/EmbedThumbnail.cs index 73fe3472d..9933183af 100644 --- a/src/Discord.Net/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net/API/Common/EmbedThumbnail.cs @@ -9,8 +9,8 @@ namespace Discord.API [JsonProperty("proxy_url")] public string ProxyUrl { get; set; } [JsonProperty("height")] - public int? Height { get; set; } + public int Height { get; set; } [JsonProperty("width")] - public int? Width { get; set; } + public int Width { get; set; } } } diff --git a/src/Discord.Net/API/Common/Game.cs b/src/Discord.Net/API/Common/Game.cs index 76a18483c..ec0fccc84 100644 --- a/src/Discord.Net/API/Common/Game.cs +++ b/src/Discord.Net/API/Common/Game.cs @@ -7,8 +7,8 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("url")] - public string StreamUrl { get; set; } + public Optional StreamUrl { get; set; } [JsonProperty("type")] - public StreamType? StreamType { get; set; } + public Optional StreamType { get; set; } } } diff --git a/src/Discord.Net/API/Common/GuildEmbed.cs b/src/Discord.Net/API/Common/GuildEmbed.cs index 9aceaa472..59f933f31 100644 --- a/src/Discord.Net/API/Common/GuildEmbed.cs +++ b/src/Discord.Net/API/Common/GuildEmbed.cs @@ -7,6 +7,6 @@ namespace Discord.API [JsonProperty("enabled")] public bool Enabled { get; set; } [JsonProperty("channel_id")] - public ulong? ChannelId { get; set; } + public ulong ChannelId { get; set; } } } diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index fb0157a90..b97775c81 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -10,12 +10,12 @@ namespace Discord.API [JsonProperty("nick")] public Optional Nick { get; set; } [JsonProperty("roles")] - public Optional Roles { get; set; } + public ulong[] Roles { get; set; } [JsonProperty("joined_at")] - public Optional JoinedAt { get; set; } + public DateTime JoinedAt { get; set; } [JsonProperty("deaf")] - public Optional Deaf { get; set; } + public bool Deaf { get; set; } [JsonProperty("mute")] - public Optional Mute { get; set; } + public bool Mute { get; set; } } } diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net/API/Common/Presence.cs index ce4edfb0f..9cf5e1d5b 100644 --- a/src/Discord.Net/API/Common/Presence.cs +++ b/src/Discord.Net/API/Common/Presence.cs @@ -7,7 +7,9 @@ namespace Discord.API [JsonProperty("user")] public User User { get; set; } [JsonProperty("guild_id")] - public ulong? GuildId { get; set; } + public Optional GuildId { get; set; } + [JsonProperty("roles")] + public Optional Roles { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } [JsonProperty("game")] diff --git a/src/Discord.Net/API/Common/ReadState.cs b/src/Discord.Net/API/Common/ReadState.cs index e4177bedf..4a5fa26ef 100644 --- a/src/Discord.Net/API/Common/ReadState.cs +++ b/src/Discord.Net/API/Common/ReadState.cs @@ -9,6 +9,6 @@ namespace Discord.API [JsonProperty("mention_count")] public int MentionCount { get; set; } [JsonProperty("last_message_id")] - public ulong? LastMessageId { get; set; } + public Optional LastMessageId { get; set; } } } diff --git a/src/Discord.Net/API/Common/Role.cs b/src/Discord.Net/API/Common/Role.cs index 721b2a50b..90931c182 100644 --- a/src/Discord.Net/API/Common/Role.cs +++ b/src/Discord.Net/API/Common/Role.cs @@ -9,14 +9,14 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("color")] - public uint? Color { get; set; } + public uint Color { get; set; } [JsonProperty("hoist")] - public bool? Hoist { get; set; } + public bool Hoist { get; set; } [JsonProperty("position")] - public int? Position { get; set; } + public int Position { get; set; } [JsonProperty("permissions"), Int53] - public ulong? Permissions { get; set; } + public ulong Permissions { get; set; } [JsonProperty("managed")] - public bool? Managed { get; set; } + public bool Managed { get; set; } } } diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 95b331b1e..04ba4a549 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -391,7 +391,7 @@ namespace Discord.API try { var model = await SendAsync("GET", $"channels/{channelId}", options: options).ConfigureAwait(false); - if (model.GuildId != guildId) + if (!model.GuildId.IsSpecified || model.GuildId.Value != guildId) return null; return model; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 0a33d9f4c..ec170c1b1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -133,7 +133,7 @@ namespace Discord var model = await ApiClient.GetChannelAsync(id).ConfigureAwait(false); if (model != null) { - if (model.GuildId != null) + if (model.GuildId.IsSpecified) { var guildModel = await ApiClient.GetGuildAsync(model.GuildId.Value).ConfigureAwait(false); if (guildModel != null) @@ -143,7 +143,7 @@ namespace Discord } } else - return new DMChannel(this, new User(this, model.Recipient), model); + return new DMChannel(this, new User(this, model.Recipient.Value), model); } return null; } @@ -151,7 +151,7 @@ namespace Discord public virtual async Task> GetDMChannelsAsync() { var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false); - return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); + return models.Select(x => new DMChannel(this, new User(this, x.Recipient.Value), x)).ToImmutableArray(); } /// diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 509139ca7..d1fb90302 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -239,7 +239,7 @@ namespace Discord } internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) { - var recipient = GetOrAddUser(model.Recipient, dataStore); + var recipient = GetOrAddUser(model.Recipient.Value, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); return channel; @@ -457,7 +457,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; - if (data.GuildId != null) + if (data.GuildId.IsSpecified) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -499,7 +499,7 @@ namespace Discord ICachedChannel channel = null; var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId != null) + if (data.GuildId.IsSpecified) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -812,13 +812,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId == null) - { - var user = DataStore.GetUser(data.User.Id); - if (user == null) - user.Update(data, UpdateSource.WebSocket); - } - else + if (data.GuildId.IsSpecified) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild == null) @@ -831,6 +825,12 @@ namespace Discord else guild.AddOrUpdatePresence(data); } + else + { + var user = DataStore.GetUser(data.User.Id); + if (user == null) + user.Update(data, UpdateSource.WebSocket); + } } break; case "TYPING_START": diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index ca9d1c1f3..50d275f44 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -30,7 +30,7 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Recipient.Update(model.Recipient, UpdateSource.Rest); + Recipient.Update(model.Recipient.Value, UpdateSource.Rest); } public async Task UpdateAsync() diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 2459b3103..2bb752078 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -34,15 +34,13 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Name = model.Name; - Position = model.Position; + Name = model.Name.Value; + Position = model.Position.Value; + var overwrites = model.PermissionOverwrites.Value; var newOverwrites = new ConcurrentDictionary(); - for (int i = 0; i < model.PermissionOverwrites.Length; i++) - { - var overwrite = model.PermissionOverwrites[i]; - newOverwrites[overwrite.TargetId] = new Overwrite(overwrite); - } + for (int i = 0; i < overwrites.Length; i++) + newOverwrites[overwrites[i].TargetId] = new Overwrite(overwrites[i]); _overwrites = newOverwrites; } diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 78ec865fe..6f01a263a 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -26,7 +26,7 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Topic = model.Topic; + Topic = model.Topic.Value; base.Update(model, source); } diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index dd6653942..745762876 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -22,8 +22,8 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; base.Update(model, source); - Bitrate = model.Bitrate; - UserLimit = model.UserLimit; + Bitrate = model.Bitrate.Value; + UserLimit = model.UserLimit.Value; } public async Task ModifyAsync(Action func) diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 0778d3597..3291a4e7d 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -288,14 +288,14 @@ namespace Discord internal GuildChannel ToChannel(API.Channel model) { - switch (model.Type) + switch (model.Type.Value) { case ChannelType.Text: return new TextChannel(this, model); case ChannelType.Voice: return new VoiceChannel(this, model); default: - throw new InvalidOperationException($"Unknown channel type: {model.Type}"); + throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); } } diff --git a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs index 45b936b22..eb897c994 100644 --- a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs @@ -4,6 +4,8 @@ namespace Discord { public interface IInviteMetadata : IInvite { + /// Gets the user that created this invite. + IUser Inviter { get; } /// Returns true if this invite was revoked. bool IsRevoked { get; } /// Returns true if users accepting this invite will be removed from the guild when they log off. diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index 1661f45d5..2f33efdd4 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -11,6 +11,7 @@ namespace Discord public int? MaxUses { get; private set; } public int Uses { get; private set; } public DateTime CreatedAt { get; private set; } + public IUser Inviter { get; private set; } public InviteMetadata(DiscordClient client, Model model) : base(client, model) @@ -21,6 +22,7 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; + Inviter = new User(Discord, model.Inviter); IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; diff --git a/src/Discord.Net/Entities/Roles/Role.cs b/src/Discord.Net/Entities/Roles/Role.cs index 9511ce67e..dd06cc507 100644 --- a/src/Discord.Net/Entities/Roles/Role.cs +++ b/src/Discord.Net/Entities/Roles/Role.cs @@ -37,11 +37,11 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; Name = model.Name; - IsHoisted = model.Hoist.Value; - IsManaged = model.Managed.Value; - Position = model.Position.Value; - Color = new Color(model.Color.Value); - Permissions = new GuildPermissions(model.Permissions.Value); + IsHoisted = model.Hoist; + IsManaged = model.Managed; + Position = model.Position; + Color = new Color(model.Color); + Permissions = new GuildPermissions(model.Permissions); } public async Task ModifyAsync(Action func) diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs index 18cc4665a..c54ca2342 100644 --- a/src/Discord.Net/Entities/Users/Game.cs +++ b/src/Discord.Net/Entities/Users/Game.cs @@ -17,6 +17,6 @@ namespace Discord public Game(string name) : this(name, null, StreamType.NotStreaming) { } internal Game(Model model) - : this(model.Name, model.StreamUrl, model.StreamType ?? StreamType.NotStreaming) { } + : this(model.Name, model.StreamUrl.GetValueOrDefault(null), model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming) { } } } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 0ea00639d..90e19614c 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.GuildMember; +using PresenceModel = Discord.API.Presence; using VoiceStateModel = Discord.API.VoiceState; namespace Discord @@ -47,29 +48,24 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - if (model.Deaf.IsSpecified) - IsDeaf = model.Deaf.Value; - if (model.Mute.IsSpecified) - IsMute = model.Mute.Value; - if (model.JoinedAt.IsSpecified) - JoinedAt = model.JoinedAt.Value; + //if (model.Deaf.IsSpecified) + IsDeaf = model.Deaf; + //if (model.Mute.IsSpecified) + IsMute = model.Mute; + //if (model.JoinedAt.IsSpecified) + JoinedAt = model.JoinedAt; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + //if (model.Roles.IsSpecified) + UpdateRoles(model.Roles); + } + public void Update(PresenceModel model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + if (model.Roles.IsSpecified) - { - var value = model.Roles.Value; - var roles = ImmutableArray.CreateBuilder(value.Length + 1); - roles.Add(Guild.EveryoneRole); - for (int i = 0; i < value.Length; i++) - { - var role = Guild.GetRole(value[i]); - if (role != null) - roles.Add(role); - } - Roles = roles.ToImmutable(); - GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); - } + UpdateRoles(model.Roles.Value); } public void Update(VoiceStateModel model, UpdateSource source) { @@ -78,6 +74,19 @@ namespace Discord IsDeaf = model.Deaf; IsMute = model.Mute; } + private void UpdateRoles(ulong[] roleIds) + { + var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); + roles.Add(Guild.EveryoneRole); + for (int i = 0; i < roleIds.Length; i++) + { + var role = Guild.GetRole(roleIds[i]); + if (role != null) + roles.Add(role); + } + Roles = roles.ToImmutable(); + GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); + } public async Task UpdateAsync() { diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index f8e3c3da0..08c9b2d38 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -79,23 +79,22 @@ namespace Discord _channels = channels; var presences = new ConcurrentDictionary(); + var members = new ConcurrentDictionary(); if (model.Presences != null) { for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdatePresence(model.Presences[i], presences); + AddOrUpdatePresence(model.Presences[i], presences, members); } - _presences = presences; - - var members = new ConcurrentDictionary(); if (model.Members != null) { for (int i = 0; i < model.Members.Length; i++) AddUser(model.Members[i], dataStore, members); - _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = model.Members.Length; + _downloaderPromise = new TaskCompletionSource(); if (!model.Large) _downloaderPromise.SetResult(true); } + _presences = presences; _members = members; var voiceStates = new ConcurrentDictionary(); @@ -125,7 +124,8 @@ namespace Discord return Discord.DataStore.RemoveChannel(id) as ICachedGuildChannel; } - public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null) + public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null, + ConcurrentDictionary members = null) { var game = model.Game != null ? new Game(model.Game) : (Game?)null; var presence = new Presence(model.Status, game); @@ -229,14 +229,14 @@ namespace Discord new internal ICachedGuildChannel ToChannel(ChannelModel model) { - switch (model.Type) + switch (model.Type.Value) { case ChannelType.Text: return new CachedTextChannel(this, model); case ChannelType.Voice: return new CachedVoiceChannel(this, model); default: - throw new InvalidOperationException($"Unknown channel type: {model.Type}"); + throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index c2cb3b6c8..7f202690e 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -15,7 +15,6 @@ "buildOptions": { "allowUnsafe": true, - "define": [ "BENCHMARK" ], "warningsAsErrors": false }, From d37a186795dd742d78dbe17cec73bebcb864926e Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 16:10:39 -0300 Subject: [PATCH 041/160] Fixed indentation --- src/Discord.Net/DiscordSocketClient.cs | 1002 ++++++++++++------------ 1 file changed, 497 insertions(+), 505 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index d1fb90302..14038bdcc 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -321,628 +321,620 @@ namespace Discord try { #endif - if (seq != null) - _lastSeq = seq.Value; - try + if (seq != null) + _lastSeq = seq.Value; + try + { + switch (opCode) { - switch (opCode) - { - case GatewayOpCode.Hello: - { - await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); - } - break; - case GatewayOpCode.HeartbeatAck: - { - await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - - var latency = (int)(Environment.TickCount - _heartbeatTime); - await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); - Latency = latency; - - await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); - } - break; - case GatewayOpCode.Dispatch: - switch (type) - { - //Global - case "READY": + case GatewayOpCode.Hello: + { + await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); + } + break; + case GatewayOpCode.HeartbeatAck: + { + await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + var latency = (int)(Environment.TickCount - _heartbeatTime); + await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); + Latency = latency; + + await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + } + break; + case GatewayOpCode.Dispatch: + switch (type) + { + //Global + case "READY": + { + await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); + + //TODO: Make downloading large guilds optional + var data = (payload as JToken).ToObject(_serializer); + var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + + var currentUser = new CachedSelfUser(this, data.User); + //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); + + for (int i = 0; i < data.Guilds.Length; i++) + AddGuild(data.Guilds[i], dataStore); + for (int i = 0; i < data.PrivateChannels.Length; i++) + AddDMChannel(data.PrivateChannels[i], dataStore); + + _sessionId = data.SessionId; + _currentUser = currentUser; + DataStore = dataStore; + + await Ready.RaiseAsync().ConfigureAwait(false); + + _connectTask.TrySetResult(true); //Signal the .Connect() call to complete + } + break; + + //Guilds + case "GUILD_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + + if (data.Unavailable == false) + type = "GUILD_AVAILABLE"; + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + CachedGuild guild; + if (data.Unavailable != false) { - await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); - - //TODO: Make downloading large guilds optional - var data = (payload as JToken).ToObject(_serializer); - var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - - var currentUser = new CachedSelfUser(this, data.User); - //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); - - for (int i = 0; i < data.Guilds.Length; i++) - AddGuild(data.Guilds[i], dataStore); - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore); - - _sessionId = data.SessionId; - _currentUser = currentUser; - DataStore = dataStore; - - await Ready.RaiseAsync().ConfigureAwait(false); - - _connectTask.TrySetResult(true); //Signal the .Connect() call to complete + guild = AddGuild(data, DataStore); + await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); } - break; - - //Guilds - case "GUILD_CREATE": + else { - var data = (payload as JToken).ToObject(_serializer); - - if (data.Unavailable == false) - type = "GUILD_AVAILABLE"; - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - CachedGuild guild; - if (data.Unavailable != false) - { - guild = AddGuild(data, DataStore); - await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); - } + guild = DataStore.GetGuild(data.Id); + if (guild != null) + guild.Update(data, UpdateSource.WebSocket, DataStore); else { - guild = DataStore.GetGuild(data.Id); - if (guild != null) - guild.Update(data, UpdateSource.WebSocket, DataStore); - else - { - await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + return; } + } - await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); + await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); + } + break; + case "GUILD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.Id); + if (guild != null) + { + var before = _enablePreUpdateEvents ? guild.Clone() : null; + guild.Update(data, UpdateSource.WebSocket); + await GuildUpdated.RaiseAsync(before, guild).ConfigureAwait(false); } - break; - case "GUILD_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild."); + return; + } + } + break; + case "GUILD_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + if (data.Unavailable == true) + type = "GUILD_UNAVAILABLE"; + await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); + + var guild = RemoveGuild(data.Id); + if (guild != null) + { + await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); + if (data.Unavailable != true) + await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); + foreach (var member in guild.Members) + member.User.RemoveRef(); + } + else + { + await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + break; + + //Channels + case "CHANNEL_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.Id); + var data = (payload as JToken).ToObject(_serializer); + ICachedChannel channel = null; + if (data.GuildId.IsSpecified) + { + var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) - { - var before = _enablePreUpdateEvents ? guild.Clone() : null; - guild.Update(data, UpdateSource.WebSocket); - await GuildUpdated.RaiseAsync(before, guild).ConfigureAwait(false); - } + guild.AddChannel(data, DataStore); else { - await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); return; } } - break; - case "GUILD_DELETE": + else + channel = AddDMChannel(data, DataStore); + if (channel != null) + await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); + } + break; + case "CHANNEL_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.Id); + if (channel != null) { - var data = (payload as JToken).ToObject(_serializer); - if (data.Unavailable == true) - type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); - - var guild = RemoveGuild(data.Id); + var before = _enablePreUpdateEvents ? channel.Clone() : null; + channel.Update(data, UpdateSource.WebSocket); + await ChannelUpdated.RaiseAsync(before, channel).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; + case "CHANNEL_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); + + ICachedChannel channel = null; + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) + { + var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) - { - await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); - if (data.Unavailable != true) - await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); - foreach (var member in guild.Members) - member.User.RemoveRef(); - } + channel = guild.RemoveChannel(data.Id); else { - await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); return; } } - break; - - //Channels - case "CHANNEL_CREATE": + else + channel = RemoveDMChannel(data.Id); + if (channel != null) + await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); + else { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_CREATE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; - var data = (payload as JToken).ToObject(_serializer); - ICachedChannel channel = null; - if (data.GuildId.IsSpecified) - { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild != null) - guild.AddChannel(data, DataStore); - else - { - await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); - return; - } - } - else - channel = AddDMChannel(data, DataStore); - if (channel != null) - await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); + //Members + case "GUILD_MEMBER_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var user = guild.AddUser(data, DataStore); + await UserJoined.RaiseAsync(user).ConfigureAwait(false); } - break; - case "CHANNEL_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.Id); - if (channel != null) - { - var before = _enablePreUpdateEvents ? channel.Clone() : null; - channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.RaiseAsync(before, channel).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); + return; } - break; - case "CHANNEL_DELETE": + } + break; + case "GUILD_MEMBER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false); - - ICachedChannel channel = null; - var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + var user = guild.GetUser(data.User.Id); + if (user != null) { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild != null) - channel = guild.RemoveChannel(data.Id); - else - { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); - return; - } + var before = _enablePreUpdateEvents ? user.Clone() : null; + user.Update(data, UpdateSource.WebSocket); + await UserUpdated.RaiseAsync(before, user).ConfigureAwait(false); } - else - channel = RemoveDMChannel(data.Id); - if (channel != null) - await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); return; } } - break; - - //Members - case "GUILD_MEMBER_ADD": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - { - var user = guild.AddUser(data, DataStore); - await UserJoined.RaiseAsync(user).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; } - break; - case "GUILD_MEMBER_UPDATE": + } + break; + case "GUILD_MEMBER_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) + var user = guild.RemoveUser(data.User.Id); + if (user != null) { - var user = guild.GetUser(data.User.Id); - if (user != null) - { - var before = _enablePreUpdateEvents ? user.Clone() : null; - user.Update(data, UpdateSource.WebSocket); - await UserUpdated.RaiseAsync(before, user).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); - return; - } + user.User.RemoveRef(); + await UserLeft.RaiseAsync(user).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); return; } } - break; - case "GUILD_MEMBER_REMOVE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - { - var user = guild.RemoveUser(data.User.Id); - if (user != null) - { - user.User.RemoveRef(); - await UserLeft.RaiseAsync(user).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); - return; - } - } - else - { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); + return; } - break; - case "GUILD_MEMBERS_CHUNK": + } + break; + case "GUILD_MEMBERS_CHUNK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false); + foreach (var memberModel in data.Members) + guild.AddUser(memberModel, DataStore); - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) + if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { - foreach (var memberModel in data.Members) - guild.AddUser(memberModel, DataStore); - - if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there - { - guild.CompleteDownloadMembers(); - await GuildDownloadedMembers.RaiseAsync(guild).ConfigureAwait(false); - } - } - else - { - await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); - return; + guild.CompleteDownloadMembers(); + await GuildDownloadedMembers.RaiseAsync(guild).ConfigureAwait(false); } } - break; - - //Roles - case "GUILD_ROLE_CREATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + break; - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - { - var role = guild.AddRole(data.Role); - await RoleCreated.RaiseAsync(role).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); - return; - } + //Roles + case "GUILD_ROLE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var role = guild.AddRole(data.Role); + await RoleCreated.RaiseAsync(role).ConfigureAwait(false); } - break; - case "GUILD_ROLE_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - { - var role = guild.GetRole(data.Role.Id); - if (role != null) - { - var before = _enablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Role, UpdateSource.WebSocket); - await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); - return; - } - } - else - { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); + return; } - break; - case "GUILD_ROLE_DELETE": + } + break; + case "GUILD_ROLE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) + var role = guild.GetRole(data.Role.Id); + if (role != null) { - var role = guild.RemoveRole(data.RoleId); - if (role != null) - await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); - else - { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); - return; - } + var before = _enablePreUpdateEvents ? role.Clone() : null; + role.Update(data.Role, UpdateSource.WebSocket); + await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); return; } } - break; - - //Bans - case "GUILD_BAN_ADD": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - await UserBanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); - else - { - await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; } - break; - case "GUILD_BAN_REMOVE": + } + break; + case "GUILD_ROLE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var guild = DataStore.GetGuild(data.GuildId); - if (guild != null) - await UserUnbanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); + var role = guild.RemoveRole(data.RoleId); + if (role != null) + await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); return; } } - break; - - //Messages - case "MESSAGE_CREATE": + else + { + await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + break; + + //Bans + case "GUILD_BAN_ADD": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_ADD)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserBanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); + else + { + await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + break; + case "GUILD_BAN_REMOVE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await UserUnbanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); + else { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + break; - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; - if (channel != null) - { - var author = channel.GetUser(data.Author.Value.Id); + //Messages + case "MESSAGE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); - if (author != null) - { - var msg = channel.AddMessage(author, data); - await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); - return; - } - } - else - { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); - return; - } - } - break; - case "MESSAGE_UPDATE": + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + var author = channel.GetUser(data.Author.Value.Id); - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; - if (channel != null) + if (author != null) { - IMessage before = null, after = null; - CachedMessage cachedMsg = channel.GetMessage(data.Id); - if (cachedMsg != null) - { - before = _enablePreUpdateEvents ? cachedMsg.Clone() : null; - cachedMsg.Update(data, UpdateSource.WebSocket); - after = cachedMsg; - } - else if (data.Author.IsSpecified) - { - //Edited message isnt in cache, create a detached one - var author = channel.GetUser(data.Author.Value.Id); - if (author != null) - after = new Message(channel, author, data); - } - if (after != null) - await MessageUpdated.RaiseAsync(before, after).ConfigureAwait(false); + var msg = channel.AddMessage(author, data); + await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); return; } } - break; - case "MESSAGE_DELETE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; - if (channel != null) + await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + IMessage before = null, after = null; + CachedMessage cachedMsg = channel.GetMessage(data.Id); + if (cachedMsg != null) { - var msg = channel.RemoveMessage(data.Id); - await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); + before = _enablePreUpdateEvents ? cachedMsg.Clone() : null; + cachedMsg.Update(data, UpdateSource.WebSocket); + after = cachedMsg; } - else + else if (data.Author.IsSpecified) { - await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); - return; + //Edited message isnt in cache, create a detached one + var author = channel.GetUser(data.Author.Value.Id); + if (author != null) + after = new Message(channel, author, data); } + if (after != null) + await MessageUpdated.RaiseAsync(before, after).ConfigureAwait(false); } - break; - - //Statuses - case "PRESENCE_UPDATE": + else + { + await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; + case "MESSAGE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var msg = channel.RemoveMessage(data.Id); + await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); + } + else { - await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; + + //Statuses + case "PRESENCE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (PRESENCE_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.IsSpecified) + { + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild == null) { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild == null) - { - await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - break; - } - if (data.Status == UserStatus.Offline) - guild.RemovePresence(data.User.Id); - else - guild.AddOrUpdatePresence(data); + await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + break; } + if (data.Status == UserStatus.Offline) + guild.RemovePresence(data.User.Id); else { - var user = DataStore.GetUser(data.User.Id); - if (user == null) - user.Update(data, UpdateSource.WebSocket); + guild.AddOrUpdatePresence(data); } } - break; - case "TYPING_START": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); + var user = DataStore.GetUser(data.User.Id); + if (user == null) + user.Update(data, UpdateSource.WebSocket); + } + } + break; + case "TYPING_START": + { + await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + var user = channel.GetUser(data.UserId); + if (user != null) + await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); + } + } + break; + + //Voice + case "VOICE_STATE_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; - if (channel != null) + var data = (payload as JToken).ToObject(_serializer); + if (data.GuildId.HasValue) + { + var guild = DataStore.GetGuild(data.GuildId.Value); + if (guild != null) { - var user = channel.GetUser(data.UserId); + if (data.ChannelId == null) + guild.RemoveVoiceState(data.UserId); + else + guild.AddOrUpdateVoiceState(data); + + var user = guild.GetUser(data.UserId); if (user != null) - await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); + user.Update(data, UpdateSource.WebSocket); } else { - await _gatewayLogger.WarningAsync("TYPING_START referenced an unknown channel.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); return; } } - break; + } + break; - //Voice - case "VOICE_STATE_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); + //Settings + case "USER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.HasValue) - { - var guild = DataStore.GetGuild(data.GuildId.Value); - if (guild != null) - { - if (data.ChannelId == null) - guild.RemoveVoiceState(data.UserId); - else - guild.AddOrUpdateVoiceState(data); - - var user = guild.GetUser(data.UserId); - if (user != null) - user.Update(data, UpdateSource.WebSocket); - /*else //Happens when a user leaves/is kicked from a guild while in a voice channel - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); - return; - }*/ - } - else - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; - } - } + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; + CurrentUser.Update(data, UpdateSource.WebSocket); + await CurrentUserUpdated.RaiseAsync(before, CurrentUser).ConfigureAwait(false); } - break; - - //Settings - case "USER_UPDATE": + else { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) - { - var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; - CurrentUser.Update(data, UpdateSource.WebSocket); - await CurrentUserUpdated.RaiseAsync(before, CurrentUser).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); - return; - } + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; } - break; - - //Ignored - case "USER_SETTINGS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); - return; - case "MESSAGE_ACK": //TODO: Add (User only) - await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); - return; - case "GUILD_EMOJIS_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); - return; - case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - return; - case "VOICE_SERVER_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - return; - case "RESUMED": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (RESUMED)").ConfigureAwait(false); - return; - - //Others - default: - await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - return; - } - break; - default: - await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); - return; - } - } - catch (Exception ex) - { - await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - return; + } + break; + + //Ignored + case "USER_SETTINGS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); + return; + case "MESSAGE_ACK": //TODO: Add (User only) + await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); + return; + case "GUILD_EMOJIS_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); + return; + case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + return; + case "VOICE_SERVER_UPDATE": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + return; + case "RESUMED": //TODO: Add + await _gatewayLogger.DebugAsync("Ignored Dispatch (RESUMED)").ConfigureAwait(false); + return; + + //Others + default: + await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + return; + } + break; + default: + await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; } + } + catch (Exception ex) + { + await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + return; + } #if BENCHMARK } finally From be354f56142a3081ed512729710f8a68b1160d62 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 17:02:23 -0300 Subject: [PATCH 042/160] Fixed errors when members haven't been downloaded. --- src/Discord.Net/DiscordSocketClient.cs | 8 +++ src/Discord.Net/Entities/Users/GuildUser.cs | 14 ++++- .../Entities/WebSocket/CachedGuild.cs | 57 ++++++++++++++----- .../Entities/WebSocket/CachedGuildUser.cs | 5 ++ 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 14038bdcc..13050fc96 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -825,6 +825,14 @@ namespace Discord else { guild.AddOrUpdatePresence(data); + if (data.Roles.IsSpecified) //Happens when a user we haven't seen before logs in + { + CachedGuildUser user = guild.GetUser(data.User.Id); + if (user == null) + guild.AddUser(data, DataStore); + else + user.Update(data, UpdateSource.WebSocket); + } } } else diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 90e19614c..8351aa57b 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -37,11 +37,19 @@ namespace Discord public DiscordClient Discord => Guild.Discord; - public GuildUser(Guild guild, User user, Model model) + private GuildUser(Guild guild, User user) { Guild = guild; User = user; - + } + public GuildUser(Guild guild, User user, Model model) + : this(guild, user) + { + Update(model, UpdateSource.Creation); + } + public GuildUser(Guild guild, User user, PresenceModel model) + : this(guild, user) + { Update(model, UpdateSource.Creation); } public void Update(Model model, UpdateSource source) @@ -63,7 +71,7 @@ namespace Discord public void Update(PresenceModel model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; - + if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 08c9b2d38..ed7af9e90 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -77,23 +77,28 @@ namespace Discord AddChannel(model.Channels[i], dataStore, channels); } _channels = channels; - - var presences = new ConcurrentDictionary(); + var members = new ConcurrentDictionary(); - if (model.Presences != null) - { - for (int i = 0; i < model.Presences.Length; i++) - AddOrUpdatePresence(model.Presences[i], presences, members); - } + var presences = new ConcurrentDictionary(); if (model.Members != null) { + DownloadedMemberCount = 0; for (int i = 0; i < model.Members.Length; i++) AddUser(model.Members[i], dataStore, members); - DownloadedMemberCount = model.Members.Length; _downloaderPromise = new TaskCompletionSource(); if (!model.Large) _downloaderPromise.SetResult(true); } + if (model.Presences != null) + { + for (int i = 0; i < model.Presences.Length; i++) + { + var presence = model.Presences[i]; + AddOrUpdatePresence(presence, presences); + if (presence.Roles.IsSpecified) + AddUser(presence, dataStore, members); //TODO: Does this ever happen? + } + } _presences = presences; _members = members; @@ -124,8 +129,7 @@ namespace Discord return Discord.DataStore.RemoveChannel(id) as ICachedGuildChannel; } - public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null, - ConcurrentDictionary members = null) + public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null) { var game = model.Game != null ? new Game(model.Game) : (Game?)null; var presence = new Presence(model.Status, game); @@ -194,10 +198,35 @@ namespace Discord public CachedGuildUser AddUser(MemberModel model, DataStore dataStore, ConcurrentDictionary members = null) { var user = Discord.GetOrAddUser(model.User, dataStore); - var member = new CachedGuildUser(this, user, model); - (members ?? _members)[user.Id] = member; - user.AddRef(); - DownloadedMemberCount++; + members = members ?? _members; + + CachedGuildUser member; + if (members.TryGetValue(model.User.Id, out member)) + member.Update(model, UpdateSource.WebSocket); + else + { + member = new CachedGuildUser(this, user, model); + members[user.Id] = member; + user.AddRef(); + DownloadedMemberCount++; + } + return member; + } + public CachedGuildUser AddUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) + { + var user = Discord.GetOrAddUser(model.User, dataStore); + members = members ?? _members; + + CachedGuildUser member; + if (members.TryGetValue(model.User.Id, out member)) + member.Update(model, UpdateSource.WebSocket); + else + { + member = new CachedGuildUser(this, user, model); + members[user.Id] = member; + user.AddRef(); + DownloadedMemberCount++; + } return member; } public CachedGuildUser GetUser(ulong id) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 104e6f1da..2a7f3326d 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -1,4 +1,5 @@ using Model = Discord.API.GuildMember; +using PresenceModel = Discord.API.Presence; namespace Discord { @@ -22,6 +23,10 @@ namespace Discord : base(guild, user, model) { } + public CachedGuildUser(CachedGuild guild, CachedPublicUser user, PresenceModel model) + : base(guild, user, model) + { + } public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; ICachedUser ICachedUser.Clone() => Clone(); From 6adb6b18820cca87a3caac4621177815360cb4e7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 17:08:15 -0300 Subject: [PATCH 043/160] Fixed OOB error in MessageCache.Download --- .../{Utilities => Entities/WebSocket}/MessageCache.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename src/Discord.Net/{Utilities => Entities/WebSocket}/MessageCache.cs (92%) diff --git a/src/Discord.Net/Utilities/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs similarity index 92% rename from src/Discord.Net/Utilities/MessageCache.cs rename to src/Discord.Net/Entities/WebSocket/MessageCache.cs index c0ddf5afd..a0fcbf62c 100644 --- a/src/Discord.Net/Utilities/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -104,11 +104,16 @@ namespace Discord return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); else { + Optional relativeId; + if (cachedMessages.Count == 0) + relativeId = fromId ?? new Optional(); + else + relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; var args = new GetChannelMessagesParams { Limit = limit - cachedMessages.Count, RelativeDirection = dir, - RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id + RelativeMessageId = relativeId }; var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetUser(x.Id), x))).ToImmutableArray(); From eed8fb3833f822b25ae63adf20f2c261b550a1bf Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 17:23:47 -0300 Subject: [PATCH 044/160] added MESSAGE_DELETE_BULK --- .../API/Gateway/MessageDeleteBulkEvent.cs | 13 ++++++++++++ src/Discord.Net/DiscordSocketClient.cs | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs diff --git a/src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs b/src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs new file mode 100644 index 000000000..8e7951530 --- /dev/null +++ b/src/Discord.Net/API/Gateway/MessageDeleteBulkEvent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + public class MessageDeleteBulkEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("ids")] + public IEnumerable Ids { get; set; } + } +} diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 13050fc96..47a554249 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -805,6 +805,27 @@ namespace Discord } } break; + case "MESSAGE_DELETE_BULK": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; + if (channel != null) + { + foreach (var id in data.Ids) + { + var msg = channel.RemoveMessage(id); + await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); + } + } + else + { + await _gatewayLogger.WarningAsync("MESSAGE_DELETE_BULK referenced an unknown channel.").ConfigureAwait(false); + return; + } + } + break; //Statuses case "PRESENCE_UPDATE": From b19f18be3b881e23f9c22bfd40596e291430e9bf Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 17:39:01 -0300 Subject: [PATCH 045/160] Added support for Heartbeat opcode --- src/Discord.Net/DiscordSocketClient.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 47a554249..f7c526f51 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -336,6 +336,13 @@ namespace Discord _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); } break; + case GatewayOpCode.Heartbeat: + { + await _gatewayLogger.DebugAsync("Received Heartbeat").ConfigureAwait(false); + + await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); + } + break; case GatewayOpCode.HeartbeatAck: { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); From f3711183096a0a7fc583264d3d7c9cd70a787040 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 17:54:48 -0300 Subject: [PATCH 046/160] Fixed channel permission resolving --- src/Discord.Net/Entities/Channels/GuildChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 2bb752078..7716f897d 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -90,14 +90,14 @@ namespace Discord public OverwritePermissions? GetPermissionOverwrite(IUser user) { Overwrite value; - if (_overwrites.TryGetValue(Id, out value)) + if (_overwrites.TryGetValue(user.Id, out value)) return value.Permissions; return null; } public OverwritePermissions? GetPermissionOverwrite(IRole role) { Overwrite value; - if (_overwrites.TryGetValue(Id, out value)) + if (_overwrites.TryGetValue(role.Id, out value)) return value.Permissions; return null; } From 2a9ac298fbdcd1a0938c7ae8cfb3527c2036840a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 19:47:27 -0300 Subject: [PATCH 047/160] Fixed handling of Administrator permission --- src/Discord.Net/Entities/Permissions/Permissions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs index 8a672489b..b4a757757 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -104,7 +104,7 @@ namespace Discord ulong resolvedPermissions = 0; ulong mask = ChannelPermissions.All(channel).RawValue; - if (user.Id == user.Guild.OwnerId || GetValue(resolvedPermissions, GuildPermission.Administrator)) + if (user.Id == user.Guild.OwnerId || GetValue(guildPermissions, GuildPermission.Administrator)) resolvedPermissions = mask; //Owners and administrators always have all permissions else { From 2fc306c2e4e2becc425cb2098c0c2fec26e8b32d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:19:37 -0300 Subject: [PATCH 048/160] Fixed more unknown user errors --- src/Discord.Net/DiscordSocketClient.cs | 2 +- src/Discord.Net/Entities/Users/GuildUser.cs | 2 +- src/Discord.Net/Entities/Users/IGuildUser.cs | 2 +- .../Entities/WebSocket/CachedGuild.cs | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index f7c526f51..98cbf3ff3 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -900,7 +900,7 @@ namespace Discord if (data.ChannelId == null) guild.RemoveVoiceState(data.UserId); else - guild.AddOrUpdateVoiceState(data); + guild.AddOrUpdateVoiceState(data, DataStore); var user = guild.GetUser(data.UserId); if (user != null) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 8351aa57b..b95b943a7 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -16,7 +16,7 @@ namespace Discord { public bool IsDeaf { get; private set; } public bool IsMute { get; private set; } - public DateTime JoinedAt { get; private set; } + public DateTime? JoinedAt { get; private set; } public string Nickname { get; private set; } public GuildPermissions GuildPermissions { get; private set; } diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index ad90b6901..424313d30 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -13,7 +13,7 @@ namespace Discord /// Returns true if the guild has muted this user. bool IsMute { get; } /// Gets when this user joined this guild. - DateTime JoinedAt { get; } + DateTime? JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } /// Gets the guild-level permissions granted to this user by their roles. diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index ed7af9e90..66106e1db 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -79,7 +79,6 @@ namespace Discord _channels = channels; var members = new ConcurrentDictionary(); - var presences = new ConcurrentDictionary(); if (model.Members != null) { DownloadedMemberCount = 0; @@ -89,24 +88,25 @@ namespace Discord if (!model.Large) _downloaderPromise.SetResult(true); } + _members = members; + + var presences = new ConcurrentDictionary(); if (model.Presences != null) { for (int i = 0; i < model.Presences.Length; i++) { var presence = model.Presences[i]; AddOrUpdatePresence(presence, presences); - if (presence.Roles.IsSpecified) - AddUser(presence, dataStore, members); //TODO: Does this ever happen? + //AddUser(presence, dataStore, members); } } _presences = presences; - _members = members; var voiceStates = new ConcurrentDictionary(); if (model.VoiceStates != null) { for (int i = 0; i < model.VoiceStates.Length; i++) - AddOrUpdateVoiceState(model.VoiceStates[i], voiceStates); + AddOrUpdateVoiceState(model.VoiceStates[i], dataStore, voiceStates); } _voiceStates = voiceStates; } @@ -165,9 +165,9 @@ namespace Discord return null; } - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, ConcurrentDictionary voiceStates = null) + public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) { - var voiceChannel = GetChannel(model.ChannelId.Value) as CachedVoiceChannel; + var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel; var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); (voiceStates ?? _voiceStates)[model.UserId] = voiceState; return voiceState; @@ -214,7 +214,6 @@ namespace Discord } public CachedGuildUser AddUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) { - var user = Discord.GetOrAddUser(model.User, dataStore); members = members ?? _members; CachedGuildUser member; @@ -222,6 +221,7 @@ namespace Discord member.Update(model, UpdateSource.WebSocket); else { + var user = Discord.GetOrAddUser(model.User, dataStore); member = new CachedGuildUser(this, user, model); members[user.Id] = member; user.AddRef(); From 4601347ec8784c7e9c204cfc051b9c288c12bcb9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:23:09 -0300 Subject: [PATCH 049/160] Moved ExtendedGuild up a level --- src/Discord.Net/API/Gateway/{Common => }/ExtendedGuild.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Discord.Net/API/Gateway/{Common => }/ExtendedGuild.cs (100%) diff --git a/src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs b/src/Discord.Net/API/Gateway/ExtendedGuild.cs similarity index 100% rename from src/Discord.Net/API/Gateway/Common/ExtendedGuild.cs rename to src/Discord.Net/API/Gateway/ExtendedGuild.cs From 031d17830363a9b4364a398f002f95e245bd475d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:30:10 -0300 Subject: [PATCH 050/160] Added nick to PRESENCE_UPDATE --- src/Discord.Net/API/Common/Presence.cs | 7 +++++-- src/Discord.Net/DiscordSocketClient.cs | 2 +- src/Discord.Net/Entities/Users/GuildUser.cs | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/API/Common/Presence.cs b/src/Discord.Net/API/Common/Presence.cs index 9cf5e1d5b..16462fa30 100644 --- a/src/Discord.Net/API/Common/Presence.cs +++ b/src/Discord.Net/API/Common/Presence.cs @@ -8,11 +8,14 @@ namespace Discord.API public User User { get; set; } [JsonProperty("guild_id")] public Optional GuildId { get; set; } - [JsonProperty("roles")] - public Optional Roles { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } [JsonProperty("game")] public Game Game { get; set; } + + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("nick")] + public Optional Nick { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 98cbf3ff3..4255a8fa2 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -853,7 +853,7 @@ namespace Discord else { guild.AddOrUpdatePresence(data); - if (data.Roles.IsSpecified) //Happens when a user we haven't seen before logs in + if (data.Roles.IsSpecified || data.Nick.IsSpecified) //Happens when a user we haven't seen before logs in { CachedGuildUser user = guild.GetUser(data.User.Id); if (user == null) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index b95b943a7..7e215c170 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -74,6 +74,8 @@ namespace Discord if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + if (model.Nick.IsSpecified) + Nickname = model.Nick.Value; } public void Update(VoiceStateModel model, UpdateSource source) { From c9ab158e48fa4b6d3bb4711c327bd4fc32389443 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:35:16 -0300 Subject: [PATCH 051/160] Updated permission debuggerdisplays --- src/Discord.Net/Entities/Permissions/ChannelPermissions.cs | 2 +- src/Discord.Net/Entities/Permissions/GuildPermissions.cs | 2 +- src/Discord.Net/Entities/Permissions/OverwritePermissions.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs index 39767179a..74938f509 100644 --- a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -131,6 +131,6 @@ namespace Discord } public override string ToString() => RawValue.ToString(); - private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; + private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } } diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs index 1f17aa31e..32aadb603 100644 --- a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -143,6 +143,6 @@ namespace Discord } public override string ToString() => RawValue.ToString(); - private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; + private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } } diff --git a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs index ee425a755..009017274 100644 --- a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs @@ -138,7 +138,7 @@ namespace Discord public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; private string DebuggerDisplay => - $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + - $"Deny {DenyValue} ({string.Join(", ", ToDenyList())})"; + $"Allow {string.Join(", ", ToAllowList())}, " + + $"Deny {string.Join(", ", ToDenyList())}"; } } From 5aa134a62300528b7a8b9460da8eaf26d82d69a3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:39:43 -0300 Subject: [PATCH 052/160] Removed IOptional --- src/Discord.Net/Utilities/IOptional.cs | 8 -------- src/Discord.Net/Utilities/Optional.cs | 4 +--- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/Discord.Net/Utilities/IOptional.cs diff --git a/src/Discord.Net/Utilities/IOptional.cs b/src/Discord.Net/Utilities/IOptional.cs deleted file mode 100644 index 47c078b66..000000000 --- a/src/Discord.Net/Utilities/IOptional.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public interface IOptional - { - object Value { get; } - bool IsSpecified { get; } - } -} diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net/Utilities/Optional.cs index 095e54bda..59ded5c5e 100644 --- a/src/Discord.Net/Utilities/Optional.cs +++ b/src/Discord.Net/Utilities/Optional.cs @@ -5,7 +5,7 @@ namespace Discord { //Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Optional : IOptional + public struct Optional { private readonly T _value; @@ -45,7 +45,5 @@ namespace Discord public static implicit operator Optional(T value) => new Optional(value); public static explicit operator T(Optional value) => value.Value; - - object IOptional.Value => Value; } } From fdb6c914e69745bab2ab0d416839cf6dfc0e66dd Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:53:00 -0300 Subject: [PATCH 053/160] Removed IOptional from contract resolver --- .../Net/Converters/DiscordContractResolver.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index e240c2017..256afe866 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -55,17 +55,16 @@ namespace Discord.Net.Converters converter = ImageConverter.Instance; else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) { - var innerType = type.GenericTypeArguments[0]; var typeInput = propInfo.DeclaringType; - var typeOutput = propInfo.PropertyType; + var innerTypeOutput = type.GenericTypeArguments[0]; - var getter = typeof(Func<,>).MakeGenericType(typeInput, typeOutput); + var getter = typeof(Func<,>).MakeGenericType(typeInput, type); var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); - var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, typeOutput); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); - var converterType = typeof(OptionalConverter<>).MakeGenericType(innerType); + var converterType = typeof(OptionalConverter<>).MakeGenericType(innerTypeOutput); converter = converterType.GetTypeInfo().GetDeclaredField("Instance").GetValue(null) as JsonConverter; } } @@ -81,9 +80,8 @@ namespace Discord.Net.Converters } private static bool ShouldSerialize(object owner, Delegate getter) - where TValue : IOptional { - return (getter as Func)((TOwner)owner).IsSpecified; + return (getter as Func>)((TOwner)owner).IsSpecified; } } } From 9c1e20dda84be815909c3edccec553548f985071 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 20:53:11 -0300 Subject: [PATCH 054/160] Added support for nulls in Optionals' DebuggerDisplay --- src/Discord.Net/Utilities/Optional.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net/Utilities/Optional.cs index 59ded5c5e..8512b28bc 100644 --- a/src/Discord.Net/Utilities/Optional.cs +++ b/src/Discord.Net/Utilities/Optional.cs @@ -41,7 +41,7 @@ namespace Discord public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0; public override string ToString() => IsSpecified ? _value?.ToString() : null; - private string DebuggerDisplay => IsSpecified ? _value.ToString() : ""; + private string DebuggerDisplay => IsSpecified ? (_value?.ToString() ?? "") : ""; public static implicit operator Optional(T value) => new Optional(value); public static explicit operator T(Optional value) => value.Value; From d2d8a689ce7c2998f6252bc156dab2c3b51fb617 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 21:00:26 -0300 Subject: [PATCH 055/160] Fixed null author when downloading messages for an uncached user --- src/Discord.Net/Entities/WebSocket/MessageCache.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index a0fcbf62c..0e01c6e26 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -116,7 +116,11 @@ namespace Discord RelativeMessageId = relativeId }; var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - return cachedMessages.Concat(downloadedMessages.Select(x => new CachedMessage(_channel, _channel.GetUser(x.Id), x))).ToImmutableArray(); + return cachedMessages.Concat(downloadedMessages.Select(x => + { + var user = _channel.GetUser(x.Id) ?? new User(_channel.Discord, x.Author.Value) as IUser; + return new CachedMessage(_channel, user, x); + })).ToImmutableArray(); } } } From d376b35f423e02ce2439279225563ce826e85aa4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 21:08:42 -0300 Subject: [PATCH 056/160] Added support for urls in GetInviteAsync --- src/Discord.Net/API/DiscordAPIClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 04ba4a549..70aaa6cfc 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -627,6 +627,14 @@ namespace Discord.API { Preconditions.NotNullOrEmpty(inviteIdOrXkcd, nameof(inviteIdOrXkcd)); + //Remove trailing slash + if (inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') + inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); + //Remove leading URL + int index = inviteIdOrXkcd.LastIndexOf('/'); + if (index >= 0) + inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); + try { return await SendAsync("GET", $"invites/{inviteIdOrXkcd}", options: options).ConfigureAwait(false); From a51f15b3a666712608ab2812615eb55f822775c4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 21:26:14 -0300 Subject: [PATCH 057/160] Fixed retrieving cached users during message downloads --- src/Discord.Net/Entities/WebSocket/MessageCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index 0e01c6e26..87b0d7910 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -118,7 +118,7 @@ namespace Discord var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); return cachedMessages.Concat(downloadedMessages.Select(x => { - var user = _channel.GetUser(x.Id) ?? new User(_channel.Discord, x.Author.Value) as IUser; + var user = _channel.GetUser(x.Author.Value.Id) ?? new User(_channel.Discord, x.Author.Value) as IUser; return new CachedMessage(_channel, user, x); })).ToImmutableArray(); } From 84157c804f293ad38dcd89aa39475b4b18e0587c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 21:30:33 -0300 Subject: [PATCH 058/160] Create fake GuildUser if a message is downloaded for a missing user. --- src/Discord.Net/Entities/Users/GuildUser.cs | 3 ++- src/Discord.Net/Entities/WebSocket/MessageCache.cs | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 7e215c170..aa9485771 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -37,10 +37,11 @@ namespace Discord public DiscordClient Discord => Guild.Discord; - private GuildUser(Guild guild, User user) + public GuildUser(Guild guild, User user) { Guild = guild; User = user; + Roles = ImmutableArray.Create(); } public GuildUser(Guild guild, User user, Model model) : this(guild, user) diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index 87b0d7910..c51088129 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -116,9 +116,19 @@ namespace Discord RelativeMessageId = relativeId }; var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); + + var guild = (_channel as ICachedGuildChannel).Guild; return cachedMessages.Concat(downloadedMessages.Select(x => { - var user = _channel.GetUser(x.Author.Value.Id) ?? new User(_channel.Discord, x.Author.Value) as IUser; + IUser user = _channel.GetUser(x.Author.Value.Id); + if (user == null) + { + var newUser = new User(_channel.Discord, x.Author.Value); + if (guild != null) + user = new GuildUser(guild, newUser); + else + user = newUser; + } return new CachedMessage(_channel, user, x); })).ToImmutableArray(); } From 227643c0fc31cce42a80685a19303cdc50750bc8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 22:06:18 -0300 Subject: [PATCH 059/160] Cleaned up docstrings, improved GetUser performance --- src/Discord.Net/API/DiscordAPIClient.cs | 2 +- src/Discord.Net/DiscordClient.cs | 3 +-- src/Discord.Net/DiscordConfig.cs | 2 -- src/Discord.Net/DiscordSocketClient.cs | 9 ++++----- src/Discord.Net/Entities/Guilds/Guild.cs | 1 - src/Discord.Net/Entities/Permissions/Permissions.cs | 2 +- src/Discord.Net/Entities/SnowflakeEntity.cs | 2 +- src/Discord.Net/Entities/Users/IUser.cs | 3 +-- .../Entities/WebSocket/CachedDMChannel.cs | 1 + .../Entities/WebSocket/CachedTextChannel.cs | 13 ++++++++----- .../Entities/WebSocket/ICachedMessageChannel.cs | 2 +- src/Discord.Net/Entities/WebSocket/MessageCache.cs | 2 +- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 2 +- .../Net/WebSockets/DefaultWebsocketClient.cs | 3 +-- 14 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 70aaa6cfc..119de17d0 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -155,7 +155,7 @@ namespace Discord.API } private async Task LogoutInternalAsync() { - //TODO: An exception here will lock the client into the unusable LoggingOut state. How should we handle? (Add same solution to both DiscordClients too) + //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. if (LoginState == LoginState.LoggedOut) return; LoginState = LoginState.LoggingOut; diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index ec170c1b1..6d80d0df3 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -41,8 +41,7 @@ namespace Discord _connectionLock = new SemaphoreSlim(1, 1); _requestQueue = new RequestQueue(); - - //TODO: Is there any better way to do this WebSocketProvider access? + ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _log.VerboseAsync("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index 54989ec63..75d5b7a21 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -3,8 +3,6 @@ using System.Reflection; namespace Discord { - //TODO: Add socket config items in their own class - public class DiscordConfig { public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 4255a8fa2..351dad850 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -361,8 +361,7 @@ namespace Discord case "READY": { await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); - - //TODO: Make downloading large guilds optional + var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); @@ -741,7 +740,7 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var author = channel.GetUser(data.Author.Value.Id); + var author = channel.GetUser(data.Author.Value.Id, true); if (author != null) { @@ -780,7 +779,7 @@ namespace Discord else if (data.Author.IsSpecified) { //Edited message isnt in cache, create a detached one - var author = channel.GetUser(data.Author.Value.Id); + var author = channel.GetUser(data.Author.Value.Id, true); if (author != null) after = new Message(channel, author, data); } @@ -879,7 +878,7 @@ namespace Discord var channel = DataStore.GetChannel(data.ChannelId) as ICachedMessageChannel; if (channel != null) { - var user = channel.GetUser(data.UserId); + var user = channel.GetUser(data.UserId, true); if (user != null) await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 3291a4e7d..44d3fa326 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -130,7 +130,6 @@ namespace Discord } public async Task ModifyChannelsAsync(IEnumerable args) { - //TODO: Update channels await Discord.ApiClient.ModifyGuildChannelsAsync(Id, args).ConfigureAwait(false); } public async Task ModifyRolesAsync(IEnumerable args) diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs index b4a757757..1ce73ccb0 100644 --- a/src/Discord.Net/Entities/Permissions/Permissions.cs +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -131,7 +131,7 @@ namespace Discord if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - //TODO: C# Typeswitch candidate + //TODO: C#7 Typeswitch candidate var textChannel = channel as ITextChannel; var voiceChannel = channel as IVoiceChannel; if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) diff --git a/src/Discord.Net/Entities/SnowflakeEntity.cs b/src/Discord.Net/Entities/SnowflakeEntity.cs index 5b67e6e80..2c1788f5b 100644 --- a/src/Discord.Net/Entities/SnowflakeEntity.cs +++ b/src/Discord.Net/Entities/SnowflakeEntity.cs @@ -4,7 +4,7 @@ namespace Discord { internal abstract class SnowflakeEntity : Entity, ISnowflakeEntity { - //TODO: Candidate for Extension Property. Lets us remove this class. + //TODO: C#7 Candidate for Extension Property. Lets us remove this class. public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); public SnowflakeEntity(ulong id) diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index 9f2709a3d..d877a4d9f 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -12,8 +12,7 @@ namespace Discord bool IsBot { get; } /// Gets the username for this user. string Username { get; } - - //TODO: CreateDMChannel is a candidate to move to IGuildUser, and User made a common class, depending on next friends list update + /// Returns a private message channel to this user, creating one if it does not already exist. Task CreateDMChannelAsync(); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 7b211bf42..568cef3d9 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -66,6 +66,7 @@ namespace Discord public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); + ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index b997ead6d..906b1bb60 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -27,12 +27,15 @@ namespace Discord public override Task> GetUsersAsync() => Task.FromResult>(Members); public override Task> GetUsersAsync(int limit, int offset) => Task.FromResult>(Members.Skip(offset).Take(limit).ToImmutableArray()); - public CachedGuildUser GetUser(ulong id) + public CachedGuildUser GetUser(ulong id, bool skipCheck = false) { - //TODO: It's slow to do a perms check here... Maybe only do it on external calls? var user = Guild.GetUser(id); - if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) - return user; + if (user != null && !skipCheck) + { + ulong perms = Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue); + if (Permissions.GetValue(perms, ChannelPermission.ReadMessages)) + return user; + } return null; } @@ -69,7 +72,7 @@ namespace Discord IReadOnlyCollection ICachedMessageChannel.Members => Members; IMessage IMessageChannel.GetCachedMessage(ulong id) => GetMessage(id); - ICachedUser ICachedMessageChannel.GetUser(ulong id) => GetUser(id); + ICachedUser ICachedMessageChannel.GetUser(ulong id, bool skipCheck) => GetUser(id, skipCheck); ICachedChannel ICachedChannel.Clone() => Clone(); } } diff --git a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs index 06cfd76fd..9704198b0 100644 --- a/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/ICachedMessageChannel.cs @@ -11,6 +11,6 @@ namespace Discord CachedMessage GetMessage(ulong id); CachedMessage RemoveMessage(ulong id); - ICachedUser GetUser(ulong id); + ICachedUser GetUser(ulong id, bool skipCheck = false); } } diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index c51088129..4fd5ea785 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -120,7 +120,7 @@ namespace Discord var guild = (_channel as ICachedGuildChannel).Guild; return cachedMessages.Concat(downloadedMessages.Select(x => { - IUser user = _channel.GetUser(x.Author.Value.Id); + IUser user = _channel.GetUser(x.Author.Value.Id, true); if (user == null) { var newUser = new User(_channel.Discord, x.Author.Value); diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index c51c5fa0a..3b133e615 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -92,7 +92,7 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { - //TODO: C# Typeswitch candidate + //TODO: C#7 Typeswitch candidate var stringValue = p.Value as string; if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } var byteArrayValue = p.Value as byte[]; diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 7408e1557..f8f8731d9 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -124,8 +124,7 @@ namespace Discord.Net.WebSockets _sendLock.Release(); } } - - //TODO: Check this code + private async Task RunAsync(CancellationToken cancelToken) { var buffer = new ArraySegment(new byte[ReceiveChunkSize]); From 610b2335d1445ba7be0db0404be94f25c0021c53 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 12 Jun 2016 23:41:13 -0300 Subject: [PATCH 060/160] Fixed more unknown user errors --- src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 906b1bb60..410d36e58 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -30,7 +30,9 @@ namespace Discord public CachedGuildUser GetUser(ulong id, bool skipCheck = false) { var user = Guild.GetUser(id); - if (user != null && !skipCheck) + if (skipCheck) return user; + + if (user != null) { ulong perms = Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue); if (Permissions.GetValue(perms, ChannelPermission.ReadMessages)) From 4cc393f9639eaa3bbb934f058a2a87a81073dc2d Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 13:49:05 -0300 Subject: [PATCH 061/160] Added Reconnect/Resume --- src/Discord.Net/API/DiscordAPIClient.cs | 16 ++- src/Discord.Net/API/Gateway/GatewayOpCode.cs | 4 +- src/Discord.Net/API/Gateway/ResumeParams.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 116 ++++++++++++++++-- src/Discord.Net/Net/WebSocketException.cs | 16 +++ .../Net/WebSockets/DefaultWebsocketClient.cs | 57 ++++++--- .../Net/WebSockets/IWebSocketClient.cs | 1 + 7 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 src/Discord.Net/Net/WebSocketException.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 119de17d0..82fc625c1 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -7,7 +7,6 @@ using Discord.Net.Queue; using Discord.Net.Rest; using Discord.Net.WebSockets; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -28,6 +27,7 @@ namespace Discord.API public event Func SentRequest; public event Func SentGatewayMessage; public event Func ReceivedGatewayEvent; + public event Func Disconnected; private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; @@ -75,6 +75,11 @@ namespace Discord.API var msg = JsonConvert.DeserializeObject(text); await ReceivedGatewayEvent.RaiseAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); }; + _gatewayClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await Disconnected.RaiseAsync(ex).ConfigureAwait(false); + }; } _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; @@ -363,6 +368,15 @@ namespace Discord.API }; await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } + public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) + { + var msg = new ResumeParams() + { + SessionId = sessionId, + Sequence = lastSeq + }; + await SendGatewayAsync(GatewayOpCode.Resume, msg, options: options).ConfigureAwait(false); + } public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) { await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net/API/Gateway/GatewayOpCode.cs b/src/Discord.Net/API/Gateway/GatewayOpCode.cs index ac1a21e1d..8b983383f 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCode.cs @@ -2,7 +2,7 @@ { public enum GatewayOpCode : byte { - /// C←S - Used to send most events. + /// S→C - Used to send most events. Dispatch = 0, /// C↔S - Used to keep the connection alive and measure latency. Heartbeat = 1, @@ -16,7 +16,7 @@ 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. + /// S→C - 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/ResumeParams.cs b/src/Discord.Net/API/Gateway/ResumeParams.cs index ba4489336..b10e312f2 100644 --- a/src/Discord.Net/API/Gateway/ResumeParams.cs +++ b/src/Discord.Net/API/Gateway/ResumeParams.cs @@ -7,6 +7,6 @@ namespace Discord.API.Gateway [JsonProperty("session_id")] public string SessionId { get; set; } [JsonProperty("seq")] - public uint Sequence { get; set; } + public int Sequence { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 351dad850..923290821 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -55,8 +55,9 @@ namespace Discord private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; private CancellationTokenSource _heartbeatCancelToken; - private Task _heartbeatTask; + private Task _heartbeatTask, _reconnectTask; private long _heartbeatTime; + private bool _isReconnecting; /// Gets the shard if of this client. public int ShardId { get; } @@ -64,9 +65,9 @@ namespace Discord public ConnectionState ConnectionState { get; private set; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } + internal IWebSocketClient GatewaySocket { get; private set; } internal int MessageCacheSize { get; private set; } - //internal bool UsePermissionCache { get; private set; } internal DataStore DataStore { get; private set; } internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; @@ -104,7 +105,6 @@ namespace Discord _dataStoreProvider = config.DataStoreProvider; MessageCacheSize = config.MessageCacheSize; - //UsePermissionCache = config.UsePermissionsCache; _enablePreUpdateEvents = config.EnablePreUpdateEvents; _largeThreshold = config.LargeThreshold; @@ -122,6 +122,16 @@ namespace Discord ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; + ApiClient.Disconnected += async ex => + { + if (ex != null) + { + await _gatewayLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + } + else + await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); + }; GatewaySocket = config.WebSocketProvider(); _voiceRegions = ImmutableDictionary.Create(); @@ -147,6 +157,7 @@ namespace Discord await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } @@ -157,6 +168,7 @@ namespace Discord throw new InvalidOperationException("You must log in before connecting."); ConnectionState = ConnectionState.Connecting; + await _gatewayLogger.InfoAsync("Connecting"); try { _connectTask = new TaskCompletionSource(); @@ -165,6 +177,7 @@ namespace Discord await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; + await _gatewayLogger.InfoAsync("Connected"); } catch (Exception) { @@ -180,6 +193,7 @@ namespace Discord await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await DisconnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } @@ -190,15 +204,62 @@ namespace Discord if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; + await _gatewayLogger.InfoAsync("Disconnecting"); + try { _heartbeatCancelToken.Cancel(); } catch { } await ApiClient.DisconnectAsync().ConfigureAwait(false); await _heartbeatTask.ConfigureAwait(false); while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; + await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); await Disconnected.RaiseAsync().ConfigureAwait(false); } + private async Task StartReconnectAsync() + { + //TODO: Is this thread-safe? + while (true) + { + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + if (_reconnectTask != null) return; + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); + } + finally { _connectionLock.Release(); } + } + } + private async Task ReconnectInternalAsync() + { + int nextReconnectDelay = 1000; + while (_isReconnecting) + { + try + { + await Task.Delay(nextReconnectDelay).ConfigureAwait(false); + nextReconnectDelay *= 2; + if (nextReconnectDelay > 30000) + nextReconnectDelay = 30000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + return; + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); + } + } + _reconnectTask = null; + } /// public override Task GetVoiceRegionAsync(string id) @@ -332,7 +393,10 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + if (_sessionId != null) + await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + else + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); } break; @@ -354,6 +418,24 @@ namespace Discord await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); } break; + case GatewayOpCode.InvalidSession: + { + await _gatewayLogger.DebugAsync("Received InvalidSession").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Failed to resume previous session"); + + _sessionId = null; + _lastSeq = 0; + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + } + break; + case GatewayOpCode.Reconnect: + { + await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("Server requested a reconnect"); + + await StartReconnectAsync().ConfigureAwait(false); + } + break; case GatewayOpCode.Dispatch: switch (type) { @@ -380,6 +462,7 @@ namespace Discord await Ready.RaiseAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete + await _gatewayLogger.InfoAsync("Ready"); } break; @@ -410,7 +493,11 @@ namespace Discord } } - await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); + if (data.Unavailable != true) + { + await _gatewayLogger.InfoAsync($"Connected to {data.Name}").ConfigureAwait(false); + await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); + } } break; case "GUILD_UPDATE": @@ -442,11 +529,17 @@ namespace Discord var guild = RemoveGuild(data.Id); if (guild != null) { + foreach (var member in guild.Members) + member.User.RemoveRef(); + await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); + await _gatewayLogger.InfoAsync($"Disconnected from {data.Name}").ConfigureAwait(false); if (data.Unavailable != true) + { await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); - foreach (var member in guild.Members) - member.User.RemoveRef(); + await _gatewayLogger.InfoAsync($"Left {data.Name}").ConfigureAwait(false); + } + } else { @@ -987,11 +1080,16 @@ namespace Discord var state = ConnectionState; while (state == ConnectionState.Connecting || state == ConnectionState.Connected) { - //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + if (_heartbeatTime != 0) //Server never responded to our last heartbeat + { + await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + return; + } _heartbeatTime = Environment.TickCount; await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); - await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } } catch (OperationCanceledException) { } diff --git a/src/Discord.Net/Net/WebSocketException.cs b/src/Discord.Net/Net/WebSocketException.cs new file mode 100644 index 000000000..d647b6c8c --- /dev/null +++ b/src/Discord.Net/Net/WebSocketException.cs @@ -0,0 +1,16 @@ +using System; +namespace Discord.Net +{ + public class WebSocketClosedException : Exception + { + public int CloseCode { get; } + public string Reason { get; } + + public WebSocketClosedException(int closeCode, string reason = null) + : base($"The server sent close {closeCode}{(reason != null ? $": \"{reason}\"" : "")}") + { + CloseCode = closeCode; + Reason = reason; + } + } +} diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index f8f8731d9..28d108cb3 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -1,5 +1,6 @@ using Discord.Extensions; using System; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net.WebSockets; @@ -17,9 +18,11 @@ namespace Discord.Net.WebSockets public event Func BinaryMessage; public event Func TextMessage; - - private readonly ClientWebSocket _client; + public event Func Closed; + private readonly SemaphoreSlim _sendLock; + private readonly Dictionary _headers; + private ClientWebSocket _client; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; @@ -27,14 +30,11 @@ namespace Discord.Net.WebSockets public DefaultWebSocketClient() { - _client = new ClientWebSocket(); - _client.Options.Proxy = null; - _client.Options.KeepAliveInterval = TimeSpan.Zero; - _sendLock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; + _headers = new Dictionary(); } private void Dispose(bool disposing) { @@ -58,6 +58,15 @@ namespace Discord.Net.WebSockets _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + foreach (var header in _headers) + { + if (header.Value != null) + _client.Options.SetRequestHeader(header.Key, header.Value); + } + await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); _task = RunAsync(_cancelToken); } @@ -66,7 +75,7 @@ namespace Discord.Net.WebSockets //Assume locked _cancelTokenSource.Cancel(); - if (_client.State == WebSocketState.Open) + if (_client != null && _client.State == WebSocketState.Open) { try { @@ -82,7 +91,7 @@ namespace Discord.Net.WebSockets public void SetHeader(string key, string value) { - _client.Options.SetRequestHeader(key, value); + _headers[key] = value; } public void SetCancelToken(CancellationToken cancelToken) { @@ -148,28 +157,36 @@ namespace Discord.Net.WebSockets throw new Exception("Connection timed out."); } - if (result.MessageType == WebSocketMessageType.Close) - throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); - else + if (result.Count > 0) stream.Write(buffer.Array, 0, result.Count); - } while (result == null || !result.EndOfMessage); var array = stream.ToArray(); - if (result.MessageType == WebSocketMessageType.Binary) - await BinaryMessage.RaiseAsync(array, 0, array.Length).ConfigureAwait(false); - else if (result.MessageType == WebSocketMessageType.Text) - { - string text = Encoding.UTF8.GetString(array, 0, array.Length); - await TextMessage.RaiseAsync(text).ConfigureAwait(false); - } - stream.Position = 0; stream.SetLength(0); + + switch (result.MessageType) + { + case WebSocketMessageType.Binary: + await BinaryMessage(array, 0, array.Length).ConfigureAwait(false); + break; + case WebSocketMessageType.Text: + string text = Encoding.UTF8.GetString(array, 0, array.Length); + await TextMessage(text).ConfigureAwait(false); + break; + case WebSocketMessageType.Close: + var _ = Closed(new WebSocketClosedException((int)result.CloseStatus, result.CloseStatusDescription)); + return; + } } } catch (OperationCanceledException) { } + catch (Exception ex) + { + //This cannot be awaited otherwise we'll deadlock when DiscordApiClient waits for this task to complete. + var _ = Closed(ex); + } } } } diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs index 583aaa06d..7eccaabf2 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -8,6 +8,7 @@ namespace Discord.Net.WebSockets { event Func BinaryMessage; event Func TextMessage; + event Func Closed; void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); From b703fba0b747464ca5d816b25b62d1456f4caf21 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 15:03:45 -0300 Subject: [PATCH 062/160] Fixed reconnect logic --- src/Discord.Net/DiscordSocketClient.cs | 44 +++++++++++++++----------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 923290821..258479d03 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -235,30 +235,37 @@ namespace Discord } private async Task ReconnectInternalAsync() { - int nextReconnectDelay = 1000; - while (_isReconnecting) + try { - try + int nextReconnectDelay = 1000; + while (_isReconnecting) { - await Task.Delay(nextReconnectDelay).ConfigureAwait(false); - nextReconnectDelay *= 2; - if (nextReconnectDelay > 30000) - nextReconnectDelay = 30000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync().ConfigureAwait(false); + await Task.Delay(nextReconnectDelay).ConfigureAwait(false); + nextReconnectDelay *= 2; + if (nextReconnectDelay > 30000) + nextReconnectDelay = 30000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + return; } - finally { _connectionLock.Release(); } - return; - } - catch (Exception ex) - { - await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); - } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); + } } + } + finally + { + _isReconnecting = false; + _reconnectTask = null; + } - _reconnectTask = null; } /// @@ -397,6 +404,7 @@ namespace Discord await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); else await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + _heartbeatTime = 0; _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); } break; From d2df6d038d53c737ca4d40fdddb3844f98e5f440 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 20:05:11 -0300 Subject: [PATCH 063/160] Added gateway url caching --- src/Discord.Net/API/DiscordAPIClient.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 82fc625c1..dd8118b11 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -36,6 +36,7 @@ namespace Discord.API private readonly SemaphoreSlim _connectionLock; private CancellationTokenSource _loginCancelToken, _connectCancelToken; private string _authToken; + private string _gatewayUrl; private bool _isDisposed; public LoginState LoginState { get; private set; } @@ -199,14 +200,18 @@ namespace Discord.API if (_gatewayClient != null) _gatewayClient.SetCancelToken(_connectCancelToken.Token); - var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); - var url = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; - await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + if (_gatewayUrl == null) + { + var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false); + _gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; + } + await _gatewayClient.ConnectAsync(_gatewayUrl).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } catch (Exception) { + _gatewayUrl = null; //Uncache in case the gateway url changed await DisconnectInternalAsync().ConfigureAwait(false); throw; } From 5ad63563c16883fda9a89637f0b6b914fad4586d Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 20:06:44 -0300 Subject: [PATCH 064/160] Added initial commands project --- Discord.Net.sln | 6 + .../Attributes/CommandAttribute.cs | 14 +++ .../Attributes/DescriptionAttribute.cs | 14 +++ .../Attributes/GroupAttribute.cs | 14 +++ .../Attributes/ModuleAttribute.cs | 9 ++ .../Attributes/UnparsedAttribute.cs | 9 ++ src/Discord.Net.Commands/CommandParser.cs | 106 ++++++++++++++++++ .../Discord.Net.Commands.xproj | 19 ++++ src/Discord.Net.Commands/project.json | 34 ++++++ 9 files changed, 225 insertions(+) create mode 100644 src/Discord.Net.Commands/Attributes/CommandAttribute.cs create mode 100644 src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs create mode 100644 src/Discord.Net.Commands/Attributes/GroupAttribute.cs create mode 100644 src/Discord.Net.Commands/Attributes/ModuleAttribute.cs create mode 100644 src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs create mode 100644 src/Discord.Net.Commands/CommandParser.cs create mode 100644 src/Discord.Net.Commands/Discord.Net.Commands.xproj create mode 100644 src/Discord.Net.Commands/project.json diff --git a/Discord.Net.sln b/Discord.Net.sln index 804e73da8..11960606b 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs new file mode 100644 index 000000000..e28017915 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Method)] + public class CommandAttribute : Attribute + { + public string Name { get; } + public CommandAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs b/src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs new file mode 100644 index 000000000..736e9720b --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/DescriptionAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)] + public class DescriptionAttribute : Attribute + { + public string Text { get; } + public DescriptionAttribute(string text) + { + Text = text; + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs new file mode 100644 index 000000000..a39437862 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Class)] + public class GroupAttribute : Attribute + { + public string Name { get; } + public GroupAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs b/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs new file mode 100644 index 000000000..5e9481a45 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Class)] + public class ModuleAttribute : Attribute + { + } +} diff --git a/src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs b/src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs new file mode 100644 index 000000000..9440b78af --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/UnparsedAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Discord.Commands +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class UnparsedAttribute : Attribute + { + } +} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs new file mode 100644 index 000000000..adee2715b --- /dev/null +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public class Module + { + public string Name { get; } + public IEnumerable Commands { get; } + + internal Module(object module, TypeInfo typeInfo) + { + List commands = new List(); + SearchClass(commands); + Commands = commands; + } + + private void SearchClass(List commands) + { + //TODO: Implement + } + } + public class Command + { + public string SourceName { get; } + } + + public class CommandParser + { + private readonly SemaphoreSlim _moduleLock; + private readonly Dictionary _modules; + + public CommandParser() + { + _modules = new Dictionary(); + _moduleLock = new SemaphoreSlim(1, 1); + } + public async Task Load(object module) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + if (_modules.ContainsKey(module)) + throw new ArgumentException($"This module has already been loaded."); + return LoadInternal(module, module.GetType().GetTypeInfo()); + } + finally + { + _moduleLock.Release(); + } + } + private Module LoadInternal(object module, TypeInfo typeInfo) + { + var loadedModule = new Module(module, typeInfo); + _modules[module] = loadedModule; + return loadedModule; + } + public async Task> LoadAssembly(Assembly assembly) + { + List modules = new List(); + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + if (typeInfo.GetCustomAttribute() != null) + { + var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); + if (constructor == null) + throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); + object module; + try { module = constructor.Invoke(null); } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + } + modules.Add(LoadInternal(module, typeInfo)); + } + } + return modules; + } + finally + { + _moduleLock.Release(); + } + } + + public async Task Unload(object module) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + return _modules.Remove(module); + } + finally + { + _moduleLock.Release(); + } + } + } +} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.xproj b/src/Discord.Net.Commands/Discord.Net.Commands.xproj new file mode 100644 index 000000000..597faf69c --- /dev/null +++ b/src/Discord.Net.Commands/Discord.Net.Commands.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 078dd7e6-943d-4d09-afc2-d2ba58b76c9c + Discord.Commands + .\obj + .\bin\ + v4.5.2 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json new file mode 100644 index 000000000..cff4b6ba5 --- /dev/null +++ b/src/Discord.Net.Commands/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0-dev", + "description": "A Discord.Net extension adding command support.", + "authors": [ "RogueException" ], + + "packOptions": { + "tags": [ "discord", "discordapp" ], + "licenseUrl": "http://opensource.org/licenses/MIT", + "projectUrl": "https://github.com/RogueException/Discord.Net", + "repository": { + "type": "git", + "url": "git://github.com/RogueException/Discord.Net" + } + }, + + "buildOptions": { + "allowUnsafe": true, + "warningsAsErrors": false + }, + + "dependencies": { + "Discord.Net": "1.0.0-dev" + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} From 3e0be761447d28ac987ec5f13911da79ac76e91b Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 20:09:17 -0300 Subject: [PATCH 065/160] Fixed CLI header --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28111dc43..11d40e881 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ You can download Discord.Net and its extensions from NuGet: ### Compiling In order to compile Discord.Net, you require the following: + #### Visual Studio 2015 - [VS2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) - [.Net Core SDK + VS Plugin](https://www.microsoft.com/net/core#windows) - NuGet 3.3+ (available through Visual Studio) + #### CLI -- [.Net Core SDK](https://www.microsoft.com/net/core#windows) \ No newline at end of file +- [.Net Core SDK](https://www.microsoft.com/net/core#windows) From a34684782610214f4ec43dfb096f4f7bcb918002 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Jun 2016 23:42:59 -0300 Subject: [PATCH 066/160] Fixed heartbeat forcing unnecessary reconnects --- src/Discord.Net/DiscordSocketClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 258479d03..145350e15 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -420,6 +420,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); var latency = (int)(Environment.TickCount - _heartbeatTime); + _heartbeatTime = 0; await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); Latency = latency; From baf1efb4469b2a89b8f4b61117bda2f3e8e0b9c9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 14 Jun 2016 21:34:41 -0300 Subject: [PATCH 067/160] Added assembly searching to commands --- src/Discord.Net.Commands/CommandParser.cs | 55 +++++++++++++++++------ src/Discord.Net/Net/Queue/RequestQueue.cs | 2 +- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index adee2715b..8157a3afd 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -12,21 +12,38 @@ namespace Discord.Commands public string Name { get; } public IEnumerable Commands { get; } - internal Module(object module, TypeInfo typeInfo) + internal Module(object parent, TypeInfo typeInfo) { List commands = new List(); - SearchClass(commands); + SearchClass(parent, commands, typeInfo); Commands = commands; } - private void SearchClass(List commands) + private void SearchClass(object parent, List commands, TypeInfo typeInfo) { - //TODO: Implement + foreach (var method in typeInfo.DeclaredMethods) + { + if (typeInfo.GetCustomAttribute() != null) + { + + } + } + foreach (var type in typeInfo.DeclaredNestedTypes) + { + if (typeInfo.GetCustomAttribute() != null) + { + SearchClass(CommandParser.CreateObject(typeInfo), commands, type); + } + } } } public class Command { public string SourceName { get; } + + internal Command(TypeInfo typeInfo) + { + } } public class CommandParser @@ -46,7 +63,10 @@ namespace Discord.Commands { if (_modules.ContainsKey(module)) throw new ArgumentException($"This module has already been loaded."); - return LoadInternal(module, module.GetType().GetTypeInfo()); + var typeInfo = module.GetType().GetTypeInfo(); + if (typeInfo.GetCustomAttribute() == null) + throw new ArgumentException($"Modules must be marked with ModuleAttribute."); + return LoadInternal(module, typeInfo); } finally { @@ -70,15 +90,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.GetCustomAttribute() != null) { - var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); - if (constructor == null) - throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); - object module; - try { module = constructor.Invoke(null); } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); - } + var module = CreateObject(typeInfo); modules.Add(LoadInternal(module, typeInfo)); } } @@ -102,5 +114,20 @@ namespace Discord.Commands _moduleLock.Release(); } } + + internal static object CreateObject(TypeInfo typeInfo) + { + var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); + if (constructor == null) + throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); + try + { + return constructor.Invoke(null); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + } + } } } diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index 631f5b457..27b11a38d 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -24,7 +24,7 @@ namespace Discord.Net.Queue _globalLimits = new Dictionary { //REST - [GlobalBucket.GeneralRest] = new BucketDefinition(0, 0), + [GlobalBucket.GeneralRest] = new BucketDefinition(0, 0), //No Limit //[GlobalBucket.Login] = new BucketDefinition(1, 1), [GlobalBucket.DirectMessage] = new BucketDefinition(5, 5), [GlobalBucket.SendEditMessage] = new BucketDefinition(50, 10), From eb7ab454b3de15b9ffbcafc89217c1b7a7f43c44 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 15 Jun 2016 12:13:30 -0300 Subject: [PATCH 068/160] Added single message GET endpoint --- src/Discord.Net/API/DiscordAPIClient.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index dd8118b11..2f7c86800 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -817,15 +817,11 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); - //TODO: Improve when Discord adds support - var msgs = await GetChannelMessagesAsync(channelId, new GetChannelMessagesParams { Limit = 1, RelativeDirection = Direction.Before, RelativeMessageId = messageId + 1 }).ConfigureAwait(false); - return msgs.FirstOrDefault(); - - /*try + try { - return await Send("GET", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); + return await SendAsync("GET", $"channels/{channelId}/messages/{messageId}", options: options).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; }*/ + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } public async Task> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null) { From ff1494f15f9fb196803c8236650232096c9c5d10 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 17 Jun 2016 22:11:39 -0300 Subject: [PATCH 069/160] Memory improvements --- src/Discord.Net/DiscordSocketClient.cs | 15 +-- src/Discord.Net/Entities/Users/GuildUser.cs | 2 +- .../Entities/WebSocket/CachedGuild.cs | 113 ++++++++---------- .../Entities/WebSocket/CachedGuildUser.cs | 16 ++- .../Entities/WebSocket/Presence.cs | 14 --- 5 files changed, 62 insertions(+), 98 deletions(-) delete mode 100644 src/Discord.Net/Entities/WebSocket/Presence.cs diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 145350e15..b7a42e748 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -949,20 +949,7 @@ namespace Discord await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); break; } - if (data.Status == UserStatus.Offline) - guild.RemovePresence(data.User.Id); - else - { - guild.AddOrUpdatePresence(data); - if (data.Roles.IsSpecified || data.Nick.IsSpecified) //Happens when a user we haven't seen before logs in - { - CachedGuildUser user = guild.GetUser(data.User.Id); - if (user == null) - guild.AddUser(data, DataStore); - else - user.Update(data, UpdateSource.WebSocket); - } - } + guild.UpdatePresence(data, DataStore); } else { diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index aa9485771..cc99fcb25 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -69,7 +69,7 @@ namespace Discord //if (model.Roles.IsSpecified) UpdateRoles(model.Roles); } - public void Update(PresenceModel model, UpdateSource source) + public virtual void Update(PresenceModel model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 66106e1db..ffe03c962 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -21,7 +21,6 @@ namespace Discord private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; - private ConcurrentDictionary _presences; private ConcurrentDictionary _voiceStates; public bool Available { get; private set; } @@ -53,12 +52,8 @@ namespace Discord _channels = new ConcurrentHashSet(); if (_members == null) _members = new ConcurrentDictionary(); - if (_presences == null) - _presences = new ConcurrentDictionary(); if (_roles == null) _roles = new ConcurrentDictionary(); - if (_voiceStates == null) - _voiceStates = new ConcurrentDictionary(); if (Emojis == null) Emojis = ImmutableArray.Create(); if (Features == null) @@ -69,17 +64,15 @@ namespace Discord base.Update(model as Model, source); MemberCount = model.MemberCount; - - var channels = new ConcurrentHashSet(); - if (model.Channels != null) + + var channels = new ConcurrentHashSet(1, (int)(model.Channels.Length * 1.05)); { for (int i = 0; i < model.Channels.Length; i++) AddChannel(model.Channels[i], dataStore, channels); } _channels = channels; - var members = new ConcurrentDictionary(); - if (model.Members != null) + var members = new ConcurrentDictionary(1, (int)(model.Presences.Length * 1.05)); { DownloadedMemberCount = 0; for (int i = 0; i < model.Members.Length; i++) @@ -87,23 +80,17 @@ namespace Discord _downloaderPromise = new TaskCompletionSource(); if (!model.Large) _downloaderPromise.SetResult(true); - } - _members = members; - var presences = new ConcurrentDictionary(); - if (model.Presences != null) - { for (int i = 0; i < model.Presences.Length; i++) { var presence = model.Presences[i]; - AddOrUpdatePresence(presence, presences); + UpdatePresence(presence, dataStore, members); //AddUser(presence, dataStore, members); } } - _presences = presences; - - var voiceStates = new ConcurrentDictionary(); - if (model.VoiceStates != null) + _members = members; + + var voiceStates = new ConcurrentDictionary(1, (int)(model.VoiceStates.Length * 1.05)); { for (int i = 0; i < model.VoiceStates.Length; i++) AddOrUpdateVoiceState(model.VoiceStates[i], dataStore, voiceStates); @@ -128,29 +115,7 @@ namespace Discord _channels.TryRemove(id); return Discord.DataStore.RemoveChannel(id) as ICachedGuildChannel; } - - public Presence AddOrUpdatePresence(PresenceModel model, ConcurrentDictionary presences = null) - { - var game = model.Game != null ? new Game(model.Game) : (Game?)null; - var presence = new Presence(model.Status, game); - (presences ?? _presences)[model.User.Id] = presence; - return presence; - } - public Presence? GetPresence(ulong id) - { - Presence presence; - if (_presences.TryGetValue(id, out presence)) - return presence; - return null; - } - public Presence? RemovePresence(ulong id) - { - Presence presence; - if (_presences.TryRemove(id, out presence)) - return presence; - return null; - } - + public Role AddRole(RoleModel model, ConcurrentDictionary roles = null) { var role = new Role(this, model); @@ -165,28 +130,6 @@ namespace Discord return null; } - public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) - { - var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); - (voiceStates ?? _voiceStates)[model.UserId] = voiceState; - return voiceState; - } - public VoiceState? GetVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) - return voiceState; - return null; - } - public VoiceState? RemoveVoiceState(ulong id) - { - VoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) - return voiceState; - return null; - } - public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); public override Task GetCurrentUserAsync() => Task.FromResult(CurrentUser); @@ -197,7 +140,6 @@ namespace Discord => Task.FromResult>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); public CachedGuildUser AddUser(MemberModel model, DataStore dataStore, ConcurrentDictionary members = null) { - var user = Discord.GetOrAddUser(model.User, dataStore); members = members ?? _members; CachedGuildUser member; @@ -205,6 +147,7 @@ namespace Discord member.Update(model, UpdateSource.WebSocket); else { + var user = Discord.GetOrAddUser(model.User, dataStore); member = new CachedGuildUser(this, user, model); members[user.Id] = member; user.AddRef(); @@ -243,6 +186,22 @@ namespace Discord return member; return null; } + public void UpdatePresence(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) + { + members = members ?? _members; + + CachedGuildUser member; + if (members.TryGetValue(model.User.Id, out member)) + member.Update(model, UpdateSource.WebSocket); + else + { + var user = Discord.GetOrAddUser(model.User, dataStore); + member = new CachedGuildUser(this, user, model); + members[user.Id] = member; + user.AddRef(); + DownloadedMemberCount++; + } + } public async Task DownloadMembersAsync() { if (!HasAllMembers) @@ -254,6 +213,28 @@ namespace Discord _downloaderPromise.TrySetResult(true); } + public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) + { + var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel; + var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + (voiceStates ?? _voiceStates)[model.UserId] = voiceState; + return voiceState; + } + public VoiceState? GetVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryGetValue(id, out voiceState)) + return voiceState; + return null; + } + public VoiceState? RemoveVoiceState(ulong id) + { + VoiceState voiceState; + if (_voiceStates.TryRemove(id, out voiceState)) + return voiceState; + return null; + } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; new internal ICachedGuildChannel ToChannel(ChannelModel model) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 2a7f3326d..13dadf5c1 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -5,13 +5,15 @@ namespace Discord { internal class CachedGuildUser : GuildUser, ICachedUser { + private Game? _game; + private UserStatus _status; + public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; public new CachedPublicUser User => base.User as CachedPublicUser; - public Presence? Presence => Guild.GetPresence(Id); - public override Game? Game => Presence?.Game; - public override UserStatus Status => Presence?.Status ?? UserStatus.Offline; + public override Game? Game => _game; + public override UserStatus Status => _status; public VoiceState? VoiceState => Guild.GetVoiceState(Id); public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; @@ -28,6 +30,14 @@ namespace Discord { } + public override void Update(PresenceModel model, UpdateSource source) + { + base.Update(model, source); + + _status = model.Status; + _game = model.Game != null ? new Game(model.Game) : (Game?)null; + } + public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; ICachedUser ICachedUser.Clone() => Clone(); } diff --git a/src/Discord.Net/Entities/WebSocket/Presence.cs b/src/Discord.Net/Entities/WebSocket/Presence.cs deleted file mode 100644 index 349751351..000000000 --- a/src/Discord.Net/Entities/WebSocket/Presence.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - internal struct Presence : IPresence - { - public UserStatus Status { get; } - public Game? Game { get; } - - public Presence(UserStatus status, Game? game) - { - Status = status; - Game = game; - } - } -} From 8df88b1bdfb4e7596a4add2f2ec12472136ca0ef Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 17 Jun 2016 22:12:46 -0300 Subject: [PATCH 070/160] Fixed .Net Framework websocket error --- src/Discord.Net/API/DiscordAPIClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index dd8118b11..4333e5f1f 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -55,7 +55,7 @@ namespace Discord.API if (webSocketProvider != null) { _gatewayClient = webSocketProvider(); - _gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) _gatewayClient.BinaryMessage += async (data, index, count) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) From e406ff4c5c172dbcb93ede75feeeee14a73e9b94 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 17 Jun 2016 22:13:20 -0300 Subject: [PATCH 071/160] Fixed direction parsing, added "around" direction --- .../Entities/Messages/Direction.cs | 3 +- .../Net/Converters/DirectionConverter.cs | 47 +++++++++++++++++++ .../Net/Converters/DiscordContractResolver.cs | 2 + 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net/Net/Converters/DirectionConverter.cs diff --git a/src/Discord.Net/Entities/Messages/Direction.cs b/src/Discord.Net/Entities/Messages/Direction.cs index c849146ff..5d8d5e621 100644 --- a/src/Discord.Net/Entities/Messages/Direction.cs +++ b/src/Discord.Net/Entities/Messages/Direction.cs @@ -3,6 +3,7 @@ public enum Direction { Before, - After + After, + Around } } diff --git a/src/Discord.Net/Net/Converters/DirectionConverter.cs b/src/Discord.Net/Net/Converters/DirectionConverter.cs new file mode 100644 index 000000000..899bd880c --- /dev/null +++ b/src/Discord.Net/Net/Converters/DirectionConverter.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.Converters +{ + public class DirectionConverter : JsonConverter + { + public static readonly DirectionConverter Instance = new DirectionConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "before": + return Direction.Before; + case "after": + return Direction.After; + case "around": + return Direction.Around; + default: + throw new JsonSerializationException("Unknown direction"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((Direction)value) + { + case Direction.Before: + writer.WriteValue("before"); + break; + case Direction.After: + writer.WriteValue("after"); + break; + case Direction.Around: + writer.WriteValue("around"); + break; + default: + throw new JsonSerializationException("Invalid direction"); + } + } + } +} diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 256afe866..be22c9b20 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -43,6 +43,8 @@ namespace Discord.Net.Converters converter = PermissionTargetConverter.Instance; else if (type == typeof(UserStatus)) converter = UserStatusConverter.Instance; + else if (type == typeof(Direction)) + converter = DirectionConverter.Instance; //Entities if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) From f24fa964aef31ba069c0d274ac4d0e2862812779 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 15:15:36 -0300 Subject: [PATCH 072/160] Added ToString and DebuggerDisplay to Game --- src/Discord.Net/Entities/Users/Game.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs index c54ca2342..e1daa542f 100644 --- a/src/Discord.Net/Entities/Users/Game.cs +++ b/src/Discord.Net/Entities/Users/Game.cs @@ -1,7 +1,9 @@ -using Model = Discord.API.Game; +using System.Diagnostics; +using Model = Discord.API.Game; namespace Discord { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Game { public string Name { get; } @@ -18,5 +20,8 @@ namespace Discord : this(name, null, StreamType.NotStreaming) { } internal Game(Model model) : this(model.Name, model.StreamUrl.GetValueOrDefault(null), model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming) { } + + public override string ToString() => Name; + private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; } } From 36845487693aa8474be038dd469f639c736eeaff Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 16:23:55 -0300 Subject: [PATCH 073/160] Several memory and performance improvements --- src/Discord.Net/API/Common/GuildMember.cs | 2 +- src/Discord.Net/API/Common/Integration.cs | 2 +- src/Discord.Net/API/Common/InviteMetadata.cs | 2 +- src/Discord.Net/API/Common/Message.cs | 4 +-- src/Discord.Net/API/Gateway/ExtendedGuild.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 32 +++++++++++-------- .../Entities/Guilds/GuildIntegration.cs | 6 ++-- .../Entities/Guilds/IGuildIntegration.cs | 3 +- src/Discord.Net/Entities/ISnowflakeEntity.cs | 2 +- .../Entities/Invites/IInviteMetadata.cs | 2 +- .../Entities/Invites/InviteMetadata.cs | 7 ++-- src/Discord.Net/Entities/Messages/IMessage.cs | 4 +-- src/Discord.Net/Entities/Messages/Message.cs | 10 +++--- src/Discord.Net/Entities/SnowflakeEntity.cs | 2 +- src/Discord.Net/Entities/Users/GuildUser.cs | 8 +++-- src/Discord.Net/Entities/Users/IGuildUser.cs | 2 +- src/Discord.Net/Entities/Users/User.cs | 7 ++-- .../Entities/WebSocket/CachedGuild.cs | 10 +++++- .../Extensions/CollectionExtensions.cs | 3 +- src/Discord.Net/Utilities/DateTimeUtils.cs | 15 ++++----- 20 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs index b97775c81..99f97990f 100644 --- a/src/Discord.Net/API/Common/GuildMember.cs +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -12,7 +12,7 @@ namespace Discord.API [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("joined_at")] - public DateTime JoinedAt { get; set; } + public DateTimeOffset JoinedAt { get; set; } [JsonProperty("deaf")] public bool Deaf { get; set; } [JsonProperty("mute")] diff --git a/src/Discord.Net/API/Common/Integration.cs b/src/Discord.Net/API/Common/Integration.cs index fca50a875..7edd70720 100644 --- a/src/Discord.Net/API/Common/Integration.cs +++ b/src/Discord.Net/API/Common/Integration.cs @@ -26,6 +26,6 @@ namespace Discord.API [JsonProperty("account")] public IntegrationAccount Account { get; set; } [JsonProperty("synced_at")] - public DateTime SyncedAt { get; set; } + public DateTimeOffset SyncedAt { get; set; } } } diff --git a/src/Discord.Net/API/Common/InviteMetadata.cs b/src/Discord.Net/API/Common/InviteMetadata.cs index 55eeebeee..fb46795bb 100644 --- a/src/Discord.Net/API/Common/InviteMetadata.cs +++ b/src/Discord.Net/API/Common/InviteMetadata.cs @@ -16,7 +16,7 @@ namespace Discord.API [JsonProperty("temporary")] public bool Temporary { get; set; } [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } [JsonProperty("revoked")] public bool Revoked { get; set; } } diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index 52950caf2..3e6d69af9 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -14,9 +14,9 @@ namespace Discord.API [JsonProperty("content")] public Optional Content { get; set; } [JsonProperty("timestamp")] - public Optional Timestamp { get; set; } + public Optional Timestamp { get; set; } [JsonProperty("edited_timestamp")] - public Optional EditedTimestamp { get; set; } + public Optional EditedTimestamp { get; set; } [JsonProperty("tts")] public Optional IsTextToSpeech { get; set; } [JsonProperty("mention_everyone")] diff --git a/src/Discord.Net/API/Gateway/ExtendedGuild.cs b/src/Discord.Net/API/Gateway/ExtendedGuild.cs index 2d91bde7e..a267295cb 100644 --- a/src/Discord.Net/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net/API/Gateway/ExtendedGuild.cs @@ -19,6 +19,6 @@ namespace Discord.API.Gateway [JsonProperty("channels")] public Channel[] Channels { get; set; } [JsonProperty("joined_at")] - public DateTime JoinedAt { get; set; } + public DateTimeOffset JoinedAt { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index b7a42e748..2a6e31e85 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -38,6 +37,7 @@ namespace Discord public event Func CurrentUserUpdated; public event Func UserIsTyping; public event Func LatencyUpdated; + //TODO: Add PresenceUpdated? VoiceStateUpdated? private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; @@ -50,6 +50,7 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; + private ConcurrentHashSet _dmChannels; private string _sessionId; private int _lastSeq; private ImmutableDictionary _voiceRegions; @@ -71,20 +72,14 @@ namespace Discord internal DataStore DataStore { get; private set; } internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; - internal IReadOnlyCollection Guilds - { - get - { - var guilds = DataStore.Guilds; - return guilds.ToReadOnlyCollection(guilds); - } - } + internal IReadOnlyCollection Guilds => DataStore.Guilds; internal IReadOnlyCollection DMChannels { get { - var users = DataStore.Users; - return users.Select(x => x.DMChannel).Where(x => x != null).ToReadOnlyCollection(users); + var dmChannels = _dmChannels; + var store = DataStore; + return dmChannels.Select(x => store.GetChannel(x) as CachedDMChannel).Where(x => x != null).ToReadOnlyCollection(dmChannels); } } internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); @@ -136,6 +131,7 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); + _dmChannels = new ConcurrentHashSet(); } protected override async Task OnLoginAsync() @@ -305,11 +301,16 @@ namespace Discord { return Task.FromResult(DataStore.GetChannel(id)); } - internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) + public override Task> GetDMChannelsAsync() + { + return Task.FromResult>(DMChannels); + } + internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet dmChannels) { var recipient = GetOrAddUser(model.Recipient.Value, dataStore); var channel = recipient.AddDMChannel(model); dataStore.AddChannel(channel); + dmChannels.TryAdd(model.Id); return channel; } internal CachedDMChannel RemoveDMChannel(ulong id) @@ -317,6 +318,7 @@ namespace Discord var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; var recipient = dmChannel.Recipient; recipient.RemoveDMChannel(id); + _dmChannels.TryRemove(id); return dmChannel; } @@ -455,6 +457,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + var dmChannels = new ConcurrentHashSet(); var currentUser = new CachedSelfUser(this, data.User); //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); @@ -462,10 +465,11 @@ namespace Discord for (int i = 0; i < data.Guilds.Length; i++) AddGuild(data.Guilds[i], dataStore); for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore); + AddDMChannel(data.PrivateChannels[i], dataStore, dmChannels); _sessionId = data.SessionId; _currentUser = currentUser; + _dmChannels = dmChannels; DataStore = dataStore; await Ready.RaiseAsync().ConfigureAwait(false); @@ -577,7 +581,7 @@ namespace Discord } } else - channel = AddDMChannel(data, DataStore); + channel = AddDMChannel(data, DataStore, _dmChannels); if (channel != null) await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index 913536fa6..b0de3518a 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -9,13 +9,14 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class GuildIntegration : Entity, IGuildIntegration { + private long _syncedAtTicks; + public string Name { get; private set; } public string Type { get; private set; } public bool IsEnabled { get; private set; } public bool IsSyncing { get; private set; } public ulong ExpireBehavior { get; private set; } public ulong ExpireGracePeriod { get; private set; } - public DateTime SyncedAt { get; private set; } public Guild Guild { get; private set; } public Role Role { get; private set; } @@ -23,6 +24,7 @@ namespace Discord public IntegrationAccount Account { get; private set; } public override DiscordClient Discord => Guild.Discord; + public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); public GuildIntegration(Guild guild, Model model) : base(model.Id) @@ -41,7 +43,7 @@ namespace Discord IsSyncing = model.Syncing; ExpireBehavior = model.ExpireBehavior; ExpireGracePeriod = model.ExpireGracePeriod; - SyncedAt = model.SyncedAt; + _syncedAtTicks = model.SyncedAt.UtcTicks; Role = Guild.GetRole(model.RoleId); User = new User(Discord, model.User); diff --git a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs index e90d8ae76..7f6ed6408 100644 --- a/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs @@ -2,6 +2,7 @@ namespace Discord { + //TODO: Add docstrings public interface IGuildIntegration { ulong Id { get; } @@ -11,7 +12,7 @@ namespace Discord bool IsSyncing { get; } ulong ExpireBehavior { get; } ulong ExpireGracePeriod { get; } - DateTime SyncedAt { get; } + DateTimeOffset SyncedAt { get; } IntegrationAccount Account { get; } IGuild Guild { get; } diff --git a/src/Discord.Net/Entities/ISnowflakeEntity.cs b/src/Discord.Net/Entities/ISnowflakeEntity.cs index 0f0f890cd..60623425c 100644 --- a/src/Discord.Net/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net/Entities/ISnowflakeEntity.cs @@ -5,6 +5,6 @@ namespace Discord public interface ISnowflakeEntity : IEntity { /// Gets when this object was created. - DateTime CreatedAt { get; } + DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs index eb897c994..1136e1678 100644 --- a/src/Discord.Net/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs @@ -17,6 +17,6 @@ namespace Discord /// Gets the amount of times this invite has been used. int Uses { get; } /// Gets when this invite was created. - DateTime CreatedAt { get; } + DateTimeOffset CreatedAt { get; } } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index 2f33efdd4..d62148fd7 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -5,14 +5,17 @@ namespace Discord { internal class InviteMetadata : Invite, IInviteMetadata { + private long _createdAtTicks; + public bool IsRevoked { get; private set; } public bool IsTemporary { get; private set; } public int? MaxAge { get; private set; } public int? MaxUses { get; private set; } public int Uses { get; private set; } - public DateTime CreatedAt { get; private set; } public IUser Inviter { get; private set; } + public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + public InviteMetadata(DiscordClient client, Model model) : base(client, model) { @@ -28,7 +31,7 @@ namespace Discord MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; MaxUses = model.MaxUses; Uses = model.Uses; - CreatedAt = model.CreatedAt; + _createdAtTicks = model.CreatedAt.UtcTicks; } } } diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 311eb17d5..0faf0837e 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -8,7 +8,7 @@ namespace Discord public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable { /// Gets the time of this message's last edit, if any. - DateTime? EditedTimestamp { get; } + DateTimeOffset? EditedTimestamp { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns the original, unprocessed text for this message. @@ -16,7 +16,7 @@ namespace Discord /// Returns the text for this message after mention processing. string Text { get; } /// Gets the time this message was sent. - DateTime Timestamp { get; } + DateTimeOffset Timestamp { get; } /// Gets the channel this message was sent to. IMessageChannel Channel { get; } diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index c9f66bfe0..e8100070d 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -12,12 +12,12 @@ namespace Discord internal class Message : SnowflakeEntity, IMessage { private bool _isMentioningEveryone; + private long _timestampTicks; + private long? _editedTimestampTicks; - public DateTime? EditedTimestamp { get; private set; } public bool IsTTS { get; private set; } public string RawText { get; private set; } public string Text { get; private set; } - public DateTime Timestamp { get; private set; } public IMessageChannel Channel { get; } public IUser Author { get; } @@ -29,6 +29,8 @@ namespace Discord public ImmutableArray MentionedUsers { get; private set; } public override DiscordClient Discord => (Channel as Entity).Discord; + public DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); public Message(IMessageChannel channel, IUser author, Model model) : base(model.Id) @@ -56,9 +58,9 @@ namespace Discord if (model.IsTextToSpeech.IsSpecified) IsTTS = model.IsTextToSpeech.Value; if (model.Timestamp.IsSpecified) - Timestamp = model.Timestamp.Value; + _timestampTicks = model.Timestamp.Value.UtcTicks; if (model.EditedTimestamp.IsSpecified) - EditedTimestamp = model.EditedTimestamp.Value; + _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.IsMentioningEveryone.IsSpecified) _isMentioningEveryone = model.IsMentioningEveryone.Value; diff --git a/src/Discord.Net/Entities/SnowflakeEntity.cs b/src/Discord.Net/Entities/SnowflakeEntity.cs index 2c1788f5b..36ed8714d 100644 --- a/src/Discord.Net/Entities/SnowflakeEntity.cs +++ b/src/Discord.Net/Entities/SnowflakeEntity.cs @@ -5,7 +5,7 @@ namespace Discord internal abstract class SnowflakeEntity : Entity, ISnowflakeEntity { //TODO: C#7 Candidate for Extension Property. Lets us remove this class. - public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public SnowflakeEntity(ulong id) : base(id) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index cc99fcb25..b0b5cfbdc 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -14,9 +14,10 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] internal class GuildUser : IGuildUser, ISnowflakeEntity { + private long? _joinedAtTicks; + public bool IsDeaf { get; private set; } public bool IsMute { get; private set; } - public DateTime? JoinedAt { get; private set; } public string Nickname { get; private set; } public GuildPermissions GuildPermissions { get; private set; } @@ -26,7 +27,7 @@ namespace Discord public ulong Id => User.Id; public string AvatarUrl => User.AvatarUrl; - public DateTime CreatedAt => User.CreatedAt; + public DateTimeOffset CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; @@ -36,6 +37,7 @@ namespace Discord public virtual Game? Game => User.Game; public DiscordClient Discord => Guild.Discord; + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); public GuildUser(Guild guild, User user) { @@ -62,7 +64,7 @@ namespace Discord //if (model.Mute.IsSpecified) IsMute = model.Mute; //if (model.JoinedAt.IsSpecified) - JoinedAt = model.JoinedAt; + _joinedAtTicks = model.JoinedAt.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 424313d30..f536b3ade 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -13,7 +13,7 @@ namespace Discord /// Returns true if the guild has muted this user. bool IsMute { get; } /// Gets when this user joined this guild. - DateTime? JoinedAt { get; } + DateTimeOffset? JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } /// Gets the guild-level permissions granted to this user by their roles. diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 70dd158c5..aa5bf8629 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -9,14 +9,15 @@ namespace Discord internal class User : SnowflakeEntity, IUser { private string _avatarId; - - public string Discriminator { get; private set; } + private ushort _discriminator; + public bool IsBot { get; private set; } public string Username { get; private set; } public override DiscordClient Discord { get; } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); + public string Discriminator => _discriminator.ToString("D4"); public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); public virtual Game? Game => null; @@ -33,7 +34,7 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; _avatarId = model.Avatar; - Discriminator = model.Discriminator; + _discriminator = ushort.Parse(model.Discriminator); IsBot = model.Bot; Username = model.Username; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index ffe03c962..1c096630f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -32,7 +32,15 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public CachedGuildUser CurrentUser => GetUser(Discord.CurrentUser.Id); - public IReadOnlyCollection Channels => _channels.Select(x => GetChannel(x)).ToReadOnlyCollection(_channels); + public IReadOnlyCollection Channels + { + get + { + var channels = _channels; + var store = Discord.DataStore; + return channels.Select(x => store.GetChannel(x) as ICachedGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); + } + } public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index 65785f643..6c81fe9cd 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -11,12 +11,13 @@ namespace Discord.Extensions public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new ConcurrentDictionaryWrapper(source, query); } - + internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection { private readonly IReadOnlyCollection _source; private readonly IEnumerable _query; + //It's okay that this count is affected by race conditions - we're wrapping a concurrent collection and that's to be expected public int Count => _source.Count; public ConcurrentDictionaryWrapper(IReadOnlyCollection source, IEnumerable query) diff --git a/src/Discord.Net/Utilities/DateTimeUtils.cs b/src/Discord.Net/Utilities/DateTimeUtils.cs index 92a42e74b..b3496520c 100644 --- a/src/Discord.Net/Utilities/DateTimeUtils.cs +++ b/src/Discord.Net/Utilities/DateTimeUtils.cs @@ -4,15 +4,12 @@ namespace Discord { internal static class DateTimeUtils { - private const ulong EpochTicks = 621355968000000000UL; - private const ulong DiscordEpochMillis = 1420070400000UL; + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); - public static DateTime FromEpochMilliseconds(ulong value) - => new DateTime((long)(value * TimeSpan.TicksPerMillisecond + EpochTicks), DateTimeKind.Utc); - public static DateTime FromEpochSeconds(ulong value) - => new DateTime((long)(value * TimeSpan.TicksPerSecond + EpochTicks), DateTimeKind.Utc); - - public static DateTime FromSnowflake(ulong value) - => FromEpochMilliseconds((value >> 22) + DiscordEpochMillis); + public static DateTimeOffset FromTicks(long ticks) + => new DateTimeOffset(ticks, TimeSpan.Zero); + public static DateTimeOffset? FromTicks(long? ticks) + => ticks != null ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : (DateTimeOffset?)null; } } From ffad33153c36edf2c6b41d774658c9b5dd1682b1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 18:53:23 -0300 Subject: [PATCH 074/160] Removed Discord reference from User --- .../Entities/Channels/DMChannel.cs | 12 ++++---- .../Entities/Channels/TextChannel.cs | 12 ++++---- src/Discord.Net/Entities/Guilds/Guild.cs | 8 ++--- .../Entities/Guilds/GuildIntegration.cs | 2 +- .../Entities/Invites/InviteMetadata.cs | 2 +- src/Discord.Net/Entities/Messages/Message.cs | 2 +- src/Discord.Net/Entities/Users/Game.cs | 2 +- src/Discord.Net/Entities/Users/GuildUser.cs | 10 +++++-- src/Discord.Net/Entities/Users/IGuildUser.cs | 3 ++ src/Discord.Net/Entities/Users/IPresence.cs | 2 +- src/Discord.Net/Entities/Users/IUser.cs | 3 -- src/Discord.Net/Entities/Users/SelfUser.cs | 2 +- src/Discord.Net/Entities/Users/User.cs | 16 +++------- .../Entities/WebSocket/CachedGuildUser.cs | 6 ++-- .../Entities/WebSocket/CachedPublicUser.cs | 29 +++++++++---------- .../Entities/WebSocket/MessageCache.cs | 4 +-- src/Discord.Net/Format.cs | 27 +++++++++++++++++ 17 files changed, 83 insertions(+), 59 deletions(-) create mode 100644 src/Discord.Net/Format.cs diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index 50d275f44..6294e3f21 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -70,7 +70,7 @@ namespace Discord { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.CreateDMMessageAsync(Id, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } public async Task SendFileAsync(string filePath, string text, bool isTTS) { @@ -79,33 +79,33 @@ namespace Discord { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadDMFileAsync(Id, file, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } } public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadDMFileAsync(Id, stream, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } public virtual async Task GetMessageAsync(ulong id) { var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); return null; } public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); } public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); } public async Task DeleteMessagesAsync(IEnumerable messages) { diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 6f01a263a..4b2fe150e 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -62,7 +62,7 @@ namespace Discord { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.CreateMessageAsync(Guild.Id, Id, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } public async Task SendFileAsync(string filePath, string text, bool isTTS) { @@ -71,33 +71,33 @@ namespace Discord { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, file, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } } public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; var model = await Discord.ApiClient.UploadFileAsync(Guild.Id, Id, stream, args).ConfigureAwait(false); - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); } public virtual async Task GetMessageAsync(ulong id) { var model = await Discord.ApiClient.GetChannelMessageAsync(Id, id).ConfigureAwait(false); if (model != null) - return new Message(this, new User(Discord, model.Author.Value), model); + return new Message(this, new User(model.Author.Value), model); return null; } public virtual async Task> GetMessagesAsync(int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); } public virtual async Task> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit) { var args = new GetChannelMessagesParams { Limit = limit }; var models = await Discord.ApiClient.GetChannelMessagesAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new Message(this, new User(Discord, x.Author.Value), x)).ToImmutableArray(); + return models.Select(x => new Message(this, new User(x.Author.Value), x)).ToImmutableArray(); } public async Task DeleteMessagesAsync(IEnumerable messages) { diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 44d3fa326..f17cb158c 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -149,7 +149,7 @@ namespace Discord public async Task> GetBansAsync() { var models = await Discord.ApiClient.GetGuildBansAsync(Id).ConfigureAwait(false); - return models.Select(x => new User(Discord, x)).ToImmutableArray(); + return models.Select(x => new User(x)).ToImmutableArray(); } public Task AddBanAsync(IUser user, int pruneDays = 0) => AddBanAsync(user, pruneDays); public async Task AddBanAsync(ulong userId, int pruneDays = 0) @@ -254,7 +254,7 @@ namespace Discord { var model = await Discord.ApiClient.GetGuildMemberAsync(Id, id).ConfigureAwait(false); if (model != null) - return new GuildUser(this, new User(Discord, model.User), model); + return new GuildUser(this, new User(model.User), model); return null; } public virtual async Task GetCurrentUserAsync() @@ -266,13 +266,13 @@ namespace Discord { var args = new GetGuildMembersParams(); var models = await Discord.ApiClient.GetGuildMembersAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); + return models.Select(x => new GuildUser(this, new User(x.User), x)).ToImmutableArray(); } public virtual async Task> GetUsersAsync(int limit, int offset) { var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; var models = await Discord.ApiClient.GetGuildMembersAsync(Id, args).ConfigureAwait(false); - return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); + return models.Select(x => new GuildUser(this, new User(x.User), x)).ToImmutableArray(); } public async Task PruneUsersAsync(int days = 30, bool simulate = false) { diff --git a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs index b0de3518a..0aba4d4e3 100644 --- a/src/Discord.Net/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/GuildIntegration.cs @@ -46,7 +46,7 @@ namespace Discord _syncedAtTicks = model.SyncedAt.UtcTicks; Role = Guild.GetRole(model.RoleId); - User = new User(Discord, model.User); + User = new User(model.User); } public async Task DeleteAsync() diff --git a/src/Discord.Net/Entities/Invites/InviteMetadata.cs b/src/Discord.Net/Entities/Invites/InviteMetadata.cs index d62148fd7..6c334a79f 100644 --- a/src/Discord.Net/Entities/Invites/InviteMetadata.cs +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -25,7 +25,7 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Inviter = new User(Discord, model.Inviter); + Inviter = new User(model.Inviter); IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index e8100070d..f737ede44 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -99,7 +99,7 @@ namespace Discord { var mentions = new User[value.Length]; for (int i = 0; i < value.Length; i++) - mentions[i] = new User(discord, value[i]); + mentions[i] = new User(value[i]); MentionedUsers = ImmutableArray.Create(mentions); } else diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs index e1daa542f..9b5d891ef 100644 --- a/src/Discord.Net/Entities/Users/Game.cs +++ b/src/Discord.Net/Entities/Users/Game.cs @@ -4,7 +4,7 @@ using Model = Discord.API.Game; namespace Discord { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Game + public class Game { public string Name { get; } public string StreamUrl { get; } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index b0b5cfbdc..bd5826473 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -34,7 +34,7 @@ namespace Discord public string Mention => User.Mention; public string Username => User.Username; public virtual UserStatus Status => User.Status; - public virtual Game? Game => User.Game; + public virtual Game Game => User.Game; public DiscordClient Discord => Guild.Discord; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); @@ -149,8 +149,14 @@ namespace Discord if (channel == null) throw new ArgumentNullException(nameof(channel)); return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); } + + public async Task CreateDMChannelAsync() + { + var args = new CreateDMChannelParams { RecipientId = Id }; + var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); - public Task CreateDMChannelAsync() => User.CreateDMChannelAsync(); + return new DMChannel(Discord, User, model); + } IGuild IGuildUser.Guild => Guild; IReadOnlyCollection IGuildUser.Roles => Roles; diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index f536b3ade..280713d33 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -31,5 +31,8 @@ namespace Discord Task KickAsync(); /// Modifies this user's properties in this guild. Task ModifyAsync(Action func); + + /// Returns a private message channel to this user, creating one if it does not already exist. + Task CreateDMChannelAsync(); } } diff --git a/src/Discord.Net/Entities/Users/IPresence.cs b/src/Discord.Net/Entities/Users/IPresence.cs index 7f182241b..af7be998a 100644 --- a/src/Discord.Net/Entities/Users/IPresence.cs +++ b/src/Discord.Net/Entities/Users/IPresence.cs @@ -3,7 +3,7 @@ public interface IPresence { /// Gets the game this user is currently playing, if any. - Game? Game { get; } + Game Game { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index d877a4d9f..44297f12e 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -12,8 +12,5 @@ namespace Discord bool IsBot { get; } /// Gets the username for this user. string Username { get; } - - /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannelAsync(); } } diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index bdd90d2ff..fed6f5e58 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -11,7 +11,7 @@ namespace Discord public bool IsVerified { get; private set; } public SelfUser(DiscordClient discord, Model model) - : base(discord, model) + : base(model) { } public override void Update(Model model, UpdateSource source) diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index aa5bf8629..3e32bb954 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -14,19 +15,18 @@ namespace Discord public bool IsBot { get; private set; } public string Username { get; private set; } - public override DiscordClient Discord { get; } + public override DiscordClient Discord { get { throw new NotSupportedException(); } } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); public string Discriminator => _discriminator.ToString("D4"); public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); - public virtual Game? Game => null; + public virtual Game Game => null; public virtual UserStatus Status => UserStatus.Unknown; - public User(DiscordClient discord, Model model) + public User(Model model) : base(model.Id) { - Discord = discord; Update(model, UpdateSource.Creation); } public virtual void Update(Model model, UpdateSource source) @@ -39,14 +39,6 @@ namespace Discord Username = model.Username; } - public async Task CreateDMChannelAsync() - { - var args = new CreateDMChannelParams { RecipientId = Id }; - var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); - - return new DMChannel(Discord, this, model); - } - public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 13dadf5c1..427ad6699 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -5,14 +5,14 @@ namespace Discord { internal class CachedGuildUser : GuildUser, ICachedUser { - private Game? _game; + private Game _game; private UserStatus _status; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; public new CachedPublicUser User => base.User as CachedPublicUser; - public override Game? Game => _game; + public override Game Game => _game; public override UserStatus Status => _status; public VoiceState? VoiceState => Guild.GetVoiceState(Id); @@ -35,7 +35,7 @@ namespace Discord base.Update(model, source); _status = model.Status; - _game = model.Game != null ? new Game(model.Game) : (Game?)null; + _game = model.Game != null ? new Game(model.Game) : (Game)null; } public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 7a3f0663d..17c05d315 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -1,5 +1,4 @@ -using Discord.Data; -using ChannelModel = Discord.API.Channel; +using ChannelModel = Discord.API.Channel; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; @@ -8,25 +7,25 @@ namespace Discord internal class CachedPublicUser : User, ICachedUser { private int _references; - private Game? _game; - private UserStatus _status; + //private Game? _game; + //private UserStatus _status; public CachedDMChannel DMChannel { get; private set; } public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public override UserStatus Status => _status; - public override Game? Game => _game; + public override UserStatus Status => UserStatus.Unknown;// _status; + public override Game Game => null; //_game; - public CachedPublicUser(DiscordSocketClient discord, Model model) - : base(discord, model) + public CachedPublicUser(Model model) + : base(model) { } - public CachedDMChannel AddDMChannel(ChannelModel model) + public CachedDMChannel AddDMChannel(DiscordSocketClient discord, ChannelModel model) { lock (this) { - var channel = new CachedDMChannel(Discord, this, model); + var channel = new CachedDMChannel(discord, this, model); DMChannel = channel; return channel; } @@ -49,10 +48,10 @@ namespace Discord { if (source == UpdateSource.Rest) return; - var game = model.Game != null ? new Game(model.Game) : (Game?)null; + //var game = model.Game != null ? new Game(model.Game) : (Game)null; - _status = model.Status; - _game = game; + //_status = model.Status; + //_game = game; } public void AddRef() @@ -60,12 +59,12 @@ namespace Discord lock (this) _references++; } - public void RemoveRef() + public void RemoveRef(DiscordSocketClient discord) { lock (this) { if (--_references == 0 && DMChannel == null) - Discord.RemoveUser(Id); + discord.RemoveUser(Id); } } diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index 4fd5ea785..7aa99bcd2 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -88,7 +88,7 @@ namespace Discord return msg; var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); if (model != null) - return new CachedMessage(_channel, new User(_discord, model.Author.Value), model); + return new CachedMessage(_channel, new User(model.Author.Value), model); return null; } public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) @@ -123,7 +123,7 @@ namespace Discord IUser user = _channel.GetUser(x.Author.Value.Id, true); if (user == null) { - var newUser = new User(_channel.Discord, x.Author.Value); + var newUser = new User(x.Author.Value); if (guild != null) user = new GuildUser(guild, newUser); else diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net/Format.cs new file mode 100644 index 000000000..fec9114a3 --- /dev/null +++ b/src/Discord.Net/Format.cs @@ -0,0 +1,27 @@ +namespace Discord +{ + namespace Discord + { + public static class Format + { + /// Returns a markdown-formatted string with bold formatting. + public static string Bold(string text) => $"**{text}**"; + /// Returns a markdown-formatted string with italics formatting. + public static string Italics(string text) => $"*{text}*"; + /// Returns a markdown-formatted string with underline formatting. + public static string Underline(string text) => $"__{text}__"; + /// Returns a markdown-formatted string with strikethrough formatting. + public static string Strikethrough(string text) => $"~~{text}~~"; + + /// Returns a markdown-formatted string with strikeout formatting. + public static string Code(string text, string language = null) + { + if (language != null || text.Contains("\n")) + return $"```{language ?? ""}\n{text}\n```"; + else + return $"`{text}`"; + } + } + } + +} From f7b4bbf41019962ba5babd1f814467391f3256de Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 18:53:40 -0300 Subject: [PATCH 075/160] Removed double namespace in Format.cs --- src/Discord.Net/Format.cs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net/Format.cs index fec9114a3..8b1d06bf8 100644 --- a/src/Discord.Net/Format.cs +++ b/src/Discord.Net/Format.cs @@ -1,27 +1,23 @@ namespace Discord { - namespace Discord + public static class Format { - public static class Format - { - /// Returns a markdown-formatted string with bold formatting. - public static string Bold(string text) => $"**{text}**"; - /// Returns a markdown-formatted string with italics formatting. - public static string Italics(string text) => $"*{text}*"; - /// Returns a markdown-formatted string with underline formatting. - public static string Underline(string text) => $"__{text}__"; - /// Returns a markdown-formatted string with strikethrough formatting. - public static string Strikethrough(string text) => $"~~{text}~~"; + /// Returns a markdown-formatted string with bold formatting. + public static string Bold(string text) => $"**{text}**"; + /// Returns a markdown-formatted string with italics formatting. + public static string Italics(string text) => $"*{text}*"; + /// Returns a markdown-formatted string with underline formatting. + public static string Underline(string text) => $"__{text}__"; + /// Returns a markdown-formatted string with strikethrough formatting. + public static string Strikethrough(string text) => $"~~{text}~~"; - /// Returns a markdown-formatted string with strikeout formatting. - public static string Code(string text, string language = null) - { - if (language != null || text.Contains("\n")) - return $"```{language ?? ""}\n{text}\n```"; - else - return $"`{text}`"; - } + /// Returns a markdown-formatted string with strikeout formatting. + public static string Code(string text, string language = null) + { + if (language != null || text.Contains("\n")) + return $"```{language ?? ""}\n{text}\n```"; + else + return $"`{text}`"; } } - } From a86acaba65961e8fa5eceaf914ae20301c131bc4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 18:54:24 -0300 Subject: [PATCH 076/160] Removed DiscordClient reference from CachedPublicUser --- src/Discord.Net/DiscordClient.cs | 10 +++++----- src/Discord.Net/DiscordSocketClient.cs | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 6d80d0df3..001c117e4 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -142,7 +142,7 @@ namespace Discord } } else - return new DMChannel(this, new User(this, model.Recipient.Value), model); + return new DMChannel(this, new User(model.Recipient.Value), model); } return null; } @@ -150,7 +150,7 @@ namespace Discord public virtual async Task> GetDMChannelsAsync() { var models = await ApiClient.GetMyDMsAsync().ConfigureAwait(false); - return models.Select(x => new DMChannel(this, new User(this, x.Recipient.Value), x)).ToImmutableArray(); + return models.Select(x => new DMChannel(this, new User(x.Recipient.Value), x)).ToImmutableArray(); } /// @@ -198,7 +198,7 @@ namespace Discord { var model = await ApiClient.GetUserAsync(id).ConfigureAwait(false); if (model != null) - return new User(this, model); + return new User(model); return null; } /// @@ -206,7 +206,7 @@ namespace Discord { var model = await ApiClient.GetUserAsync(username, discriminator).ConfigureAwait(false); if (model != null) - return new User(this, model); + return new User(model); return null; } /// @@ -225,7 +225,7 @@ namespace Discord public virtual async Task> QueryUsersAsync(string query, int limit) { var models = await ApiClient.QueryUsersAsync(query, limit).ConfigureAwait(false); - return models.Select(x => new User(this, x)).ToImmutableArray(); + return models.Select(x => new User(x)).ToImmutableArray(); } /// diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 2a6e31e85..78439acc9 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -13,6 +13,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics; namespace Discord { @@ -308,7 +309,7 @@ namespace Discord internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet dmChannels) { var recipient = GetOrAddUser(model.Recipient.Value, dataStore); - var channel = recipient.AddDMChannel(model); + var channel = recipient.AddDMChannel(this, model); dataStore.AddChannel(channel); dmChannels.TryAdd(model.Id); return channel; @@ -334,7 +335,7 @@ namespace Discord } internal CachedPublicUser GetOrAddUser(API.User model, DataStore dataStore) { - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)); + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(model)); user.AddRef(); return user; } @@ -543,7 +544,7 @@ namespace Discord if (guild != null) { foreach (var member in guild.Members) - member.User.RemoveRef(); + member.User.RemoveRef(this); await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Disconnected from {data.Name}").ConfigureAwait(false); @@ -692,7 +693,7 @@ namespace Discord var user = guild.RemoveUser(data.User.Id); if (user != null) { - user.User.RemoveRef(); + user.User.RemoveRef(this); await UserLeft.RaiseAsync(user).ConfigureAwait(false); } else @@ -813,7 +814,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserBanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); + await UserBanned.RaiseAsync(new User(data)).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); @@ -828,7 +829,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserUnbanned.RaiseAsync(new User(this, data)).ConfigureAwait(false); + await UserUnbanned.RaiseAsync(new User(data)).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); From ab9a702218ab68f393b8a09e3a0068a45a17e7ca Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 18:55:00 -0300 Subject: [PATCH 077/160] Improved WebSocket receive performance and reduced allocations --- .../Net/WebSockets/DefaultWebsocketClient.cs | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index 28d108cb3..cb31b095c 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -1,5 +1,4 @@ -using Discord.Extensions; -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -12,7 +11,7 @@ namespace Discord.Net.WebSockets { public class DefaultWebSocketClient : IWebSocketClient { - public const int ReceiveChunkSize = 12 * 1024; //12KB + public const int ReceiveChunkSize = 16 * 1024; //16KB public const int SendChunkSize = 4 * 1024; //4KB private const int HR_TIMEOUT = -2147012894; @@ -137,50 +136,64 @@ namespace Discord.Net.WebSockets private async Task RunAsync(CancellationToken cancelToken) { var buffer = new ArraySegment(new byte[ReceiveChunkSize]); - var stream = new MemoryStream(); try { while (!cancelToken.IsCancellationRequested) { - WebSocketReceiveResult result = null; - do + WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + byte[] result; + int resultCount; + + if (socketResult.MessageType == WebSocketMessageType.Close) { - if (cancelToken.IsCancellationRequested) return; + var _ = Closed(new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription)); + return; + } - try - { - result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); - } - catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + if (!socketResult.EndOfMessage) + { + //This is a large message (likely just READY), lets create a temporary expandable stream + using (var stream = new MemoryStream()) { - throw new Exception("Connection timed out."); + stream.Write(buffer.Array, 0, socketResult.Count); + do + { + if (cancelToken.IsCancellationRequested) return; + socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + stream.Write(buffer.Array, 0, socketResult.Count); + } + while (socketResult == null || !socketResult.EndOfMessage); + + //Use the internal buffer if we can get it + resultCount = (int)stream.Length; + ArraySegment streamBuffer; + if (stream.TryGetBuffer(out streamBuffer)) + result = streamBuffer.Array; + else + result = stream.ToArray(); } - - if (result.Count > 0) - stream.Write(buffer.Array, 0, result.Count); } - while (result == null || !result.EndOfMessage); - - var array = stream.ToArray(); - stream.Position = 0; - stream.SetLength(0); + else + { + //Small message + resultCount = socketResult.Count; + result = buffer.Array; + } - switch (result.MessageType) + if (socketResult.MessageType == WebSocketMessageType.Text) { - case WebSocketMessageType.Binary: - await BinaryMessage(array, 0, array.Length).ConfigureAwait(false); - break; - case WebSocketMessageType.Text: - string text = Encoding.UTF8.GetString(array, 0, array.Length); - await TextMessage(text).ConfigureAwait(false); - break; - case WebSocketMessageType.Close: - var _ = Closed(new WebSocketClosedException((int)result.CloseStatus, result.CloseStatusDescription)); - return; + string text = Encoding.UTF8.GetString(result, 0, resultCount); + await TextMessage(text).ConfigureAwait(false); } + else + await BinaryMessage(result, 0, resultCount).ConfigureAwait(false); } } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + var _ = Closed(new Exception("Connection timed out.", ex)); + } catch (OperationCanceledException) { } catch (Exception ex) { From 480034feedf762d4bd808fb1e09a941e97893c14 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 18:55:12 -0300 Subject: [PATCH 078/160] Added command map --- src/Discord.Net.Commands/CommandMap.cs | 71 +++++++++++++++++++ .../CommandSearchResults.cs | 16 +++++ 2 files changed, 87 insertions(+) create mode 100644 src/Discord.Net.Commands/CommandMap.cs create mode 100644 src/Discord.Net.Commands/CommandSearchResults.cs diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs new file mode 100644 index 000000000..f99073189 --- /dev/null +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Commands +{ + public class CommandMap + { + private readonly ConcurrentDictionary> _map; + + public CommandMap() + { + _map = new ConcurrentDictionary>(); + } + + public void Add(string key, Command cmd) + { + var list = _map.GetOrAdd(key, _ => new List()); + lock (list) + list.Add(cmd); + } + public void Remove(string key, Command cmd) + { + List list; + if (_map.TryGetValue(key, out list)) + { + lock (list) + list.Remove(cmd); + } + } + public IReadOnlyList Get(string key) + { + List list; + if (_map.TryGetValue(key, out list)) + { + lock (list) + return list.ToImmutableArray(); + } + return ImmutableArray.Create(); + } + + //TODO: C#7 Candidate for tuple + public CommandSearchResults Search(string input) + { + string lowerInput = input.ToLowerInvariant(); + + List bestGroup = null, group; + int startPos = 0, endPos; + + while (true) + { + endPos = input.IndexOf(' ', startPos); + string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); + startPos = endPos + 1; + if (!_map.TryGetValue(cmdText, out group)) + break; + bestGroup = group; + } + + ImmutableArray cmds; + if (bestGroup != null) + { + lock (bestGroup) + cmds = bestGroup.ToImmutableArray(); + } + else + cmds = ImmutableArray.Create(); + return new CommandSearchResults(cmds, startPos); + } + } +} diff --git a/src/Discord.Net.Commands/CommandSearchResults.cs b/src/Discord.Net.Commands/CommandSearchResults.cs new file mode 100644 index 000000000..4e1cbe025 --- /dev/null +++ b/src/Discord.Net.Commands/CommandSearchResults.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Discord.Commands +{ + public struct CommandSearchResults + { + IReadOnlyList Commands { get; } + int ArgsPos { get; } + + public CommandSearchResults(IReadOnlyList commands, int argsPos) + { + Commands = commands; + ArgsPos = argsPos; + } + } +} From 1b5d0546a70da4ef88dedd0badc7800781fe17a6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 19 Jun 2016 19:01:43 -0300 Subject: [PATCH 079/160] Redirected SocketClient's GetGuildEmbed, GetGuilds and GetVoiceRegions to the cache. --- src/Discord.Net/DiscordSocketClient.cs | 19 ++++++++++++++++++- .../Entities/WebSocket/CachedGuild.cs | 7 +++++-- .../Entities/WebSocket/CachedPublicUser.cs | 1 + 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 78439acc9..a4789c1bc 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -279,6 +279,18 @@ namespace Discord { return Task.FromResult(DataStore.GetGuild(id)); } + public override Task GetGuildEmbedAsync(ulong id) + { + var guild = DataStore.GetGuild(id); + if (guild != null) + return Task.FromResult(new GuildEmbed(guild.IsEmbeddable, guild.EmbedChannelId)); + else + return Task.FromResult(null); + } + public override Task> GetGuildsAsync() + { + return Task.FromResult>(Guilds); + } internal CachedGuild AddGuild(API.Gateway.ExtendedGuild model, DataStore dataStore) { var guild = new CachedGuild(this, model, dataStore); @@ -296,7 +308,7 @@ namespace Discord guild.RemoveUser(user.Id); return guild; } - + /// public override Task GetChannelAsync(ulong id) { @@ -385,6 +397,11 @@ namespace Discord } } + public override Task> GetVoiceRegionsAsync() + { + return Task.FromResult>(_voiceRegions.ToReadOnlyCollection()); + } + private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload) { #if BENCHMARK diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 1c096630f..5bd9ae06b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -16,7 +16,7 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { - internal class CachedGuild : Guild, ICachedEntity + internal class CachedGuild : Guild, IUserGuild, ICachedEntity { private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; @@ -42,7 +42,7 @@ namespace Discord } } public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); - + public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { _downloaderPromise = new TaskCompletionSource(); @@ -257,5 +257,8 @@ namespace Discord throw new InvalidOperationException($"Unknown channel type: {model.Type.Value}"); } } + + bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; + GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs index 17c05d315..915c897a4 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs @@ -6,6 +6,7 @@ namespace Discord { internal class CachedPublicUser : User, ICachedUser { + //TODO: Fix removed game/status (add CachedDMUser?) private int _references; //private Game? _game; //private UserStatus _status; From c8f9372113cebae1d0c26435867d08e90775bcea Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Jun 2016 17:00:15 -0300 Subject: [PATCH 080/160] Fixed ban event deserialization --- src/Discord.Net/API/Gateway/GuildBanEvent.cs | 4 +++- src/Discord.Net/DiscordSocketClient.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/API/Gateway/GuildBanEvent.cs b/src/Discord.Net/API/Gateway/GuildBanEvent.cs index 4d55d3c2a..5ad7534a2 100644 --- a/src/Discord.Net/API/Gateway/GuildBanEvent.cs +++ b/src/Discord.Net/API/Gateway/GuildBanEvent.cs @@ -2,9 +2,11 @@ namespace Discord.API.Gateway { - public class GuildBanEvent : User + public class GuildBanEvent { [JsonProperty("guild_id")] public ulong GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index a4789c1bc..443ba0890 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -831,7 +831,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserBanned.RaiseAsync(new User(data)).ConfigureAwait(false); + await UserBanned.RaiseAsync(new User(data.User)).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); @@ -846,7 +846,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserUnbanned.RaiseAsync(new User(data)).ConfigureAwait(false); + await UserUnbanned.RaiseAsync(new User(data.User)).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); From ad1ba0ab508453db5a74b0d5a5e57028669ce481 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Jun 2016 21:14:19 -0300 Subject: [PATCH 081/160] Added DMChannels to DataStore --- src/Discord.Net/Data/DefaultDataStore.cs | 36 ++++++++++++++++++++---- src/Discord.Net/Data/IDataStore.cs | 4 +++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DefaultDataStore.cs index 14dbcae31..c1d768ed9 100644 --- a/src/Discord.Net/Data/DefaultDataStore.cs +++ b/src/Discord.Net/Data/DefaultDataStore.cs @@ -8,12 +8,13 @@ namespace Discord.Data { public class DefaultDataStore : DataStore { + private const int CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2? private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 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 const double CollectionMultiplier = 1.05; //Add 5% buffer to handle growth private readonly ConcurrentDictionary _channels; + private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; @@ -25,9 +26,10 @@ namespace Discord.Data { double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; - _channels = new ConcurrentDictionary(1, (int)(estimatedChannelCount * CollectionMultiplier)); - _guilds = new ConcurrentDictionary(1, (int)(guildCount * CollectionMultiplier)); - _users = new ConcurrentDictionary(1, (int)(estimatedUsersCount * CollectionMultiplier)); + _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); + _dmChannels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); + _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); + _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); } internal override ICachedChannel GetChannel(ulong id) @@ -49,6 +51,30 @@ namespace Discord.Data return null; } + internal override CachedDMChannel GetDMChannel(ulong userId) + { + CachedDMChannel channel; + if (_dmChannels.TryGetValue(userId, out channel)) + return channel; + return null; + } + internal override void AddDMChannel(CachedDMChannel channel) + { + _channels[channel.Id] = channel; + _dmChannels[channel.Recipient.Id] = channel; + } + internal override CachedDMChannel RemoveDMChannel(ulong userId) + { + CachedDMChannel channel; + ICachedChannel ignored; + if (_dmChannels.TryRemove(userId, out channel)) + { + if (_channels.TryRemove(channel.Id, out ignored)) + return channel; + } + return null; + } + internal override CachedGuild GetGuild(ulong id) { CachedGuild guild; diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs index cc849cd94..9d7525a35 100644 --- a/src/Discord.Net/Data/IDataStore.cs +++ b/src/Discord.Net/Data/IDataStore.cs @@ -13,6 +13,10 @@ namespace Discord.Data internal abstract void AddChannel(ICachedChannel channel); internal abstract ICachedChannel RemoveChannel(ulong id); + internal abstract CachedDMChannel GetDMChannel(ulong userId); + internal abstract void AddDMChannel(CachedDMChannel channel); + internal abstract CachedDMChannel RemoveDMChannel(ulong userId); + internal abstract CachedGuild GetGuild(ulong id); internal abstract void AddGuild(CachedGuild guild); internal abstract CachedGuild RemoveGuild(ulong id); From 8bd304b01820e4fe99365ada17752033d40b6776 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Jun 2016 23:40:04 -0300 Subject: [PATCH 082/160] Disabled memory cache by default --- src/Discord.Net/DiscordSocketConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs index 760f9818c..04c3a0828 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -18,7 +18,7 @@ namespace Discord public int FailedReconnectDelay { get; set; } = 15000; /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. - public int MessageCacheSize { get; set; } = 100; + public int MessageCacheSize { get; set; } = 0; /*/// /// Gets or sets whether the permissions cache should be used. /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage. From 732d30e598a04b6d665dbde619b49ad39a3d51d8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:07:47 -0300 Subject: [PATCH 083/160] Added DataStore.DMChannels property --- src/Discord.Net/Data/DefaultDataStore.cs | 2 +- src/Discord.Net/Data/IDataStore.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DefaultDataStore.cs index c1d768ed9..20e804a68 100644 --- a/src/Discord.Net/Data/DefaultDataStore.cs +++ b/src/Discord.Net/Data/DefaultDataStore.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; namespace Discord.Data { @@ -19,6 +18,7 @@ namespace Discord.Data private readonly ConcurrentDictionary _users; internal override IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); + internal override IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal override IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs index 9d7525a35..f10507f3c 100644 --- a/src/Discord.Net/Data/IDataStore.cs +++ b/src/Discord.Net/Data/IDataStore.cs @@ -6,6 +6,7 @@ namespace Discord.Data public abstract class DataStore { internal abstract IReadOnlyCollection Channels { get; } + internal abstract IReadOnlyCollection DMChannels { get; } internal abstract IReadOnlyCollection Guilds { get; } internal abstract IReadOnlyCollection Users { get; } From b38455f4277a512dbd473220a57e9a94ff939e01 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:09:50 -0300 Subject: [PATCH 084/160] Several performance/memory improvements. Renamed CachedPublicUser -> CachedGlobalUser. --- src/Discord.Net/Data/DefaultDataStore.cs | 16 +- src/Discord.Net/Data/IDataStore.cs | 8 +- src/Discord.Net/DiscordSocketClient.cs | 177 +++++++++++------- .../Entities/Channels/DMChannel.cs | 11 +- .../Entities/Channels/GuildChannel.cs | 50 +++-- src/Discord.Net/Entities/Users/GuildUser.cs | 5 +- src/Discord.Net/Entities/Users/User.cs | 4 +- .../Entities/WebSocket/CachedDMChannel.cs | 11 +- .../Entities/WebSocket/CachedDMUser.cs | 38 ++++ .../Entities/WebSocket/CachedGlobalUser.cs | 39 ++++ .../Entities/WebSocket/CachedGuildUser.cs | 6 +- .../Entities/WebSocket/CachedPublicUser.cs | 75 -------- .../Entities/WebSocket/CachedTextChannel.cs | 7 +- .../Entities/WebSocket/MessageCache.cs | 69 +------ .../Entities/WebSocket/MessageManager.cs | 81 ++++++++ 15 files changed, 348 insertions(+), 249 deletions(-) create mode 100644 src/Discord.Net/Entities/WebSocket/CachedDMUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs delete mode 100644 src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/MessageManager.cs diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DefaultDataStore.cs index 20e804a68..b267f5932 100644 --- a/src/Discord.Net/Data/DefaultDataStore.cs +++ b/src/Discord.Net/Data/DefaultDataStore.cs @@ -15,12 +15,12 @@ namespace Discord.Data private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; - private readonly ConcurrentDictionary _users; + private readonly ConcurrentDictionary _users; internal override IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal override IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal override IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); - internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); public DefaultDataStore(int guildCount, int dmChannelCount) { @@ -29,7 +29,7 @@ namespace Discord.Data _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); _dmChannels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); - _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); + _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); } internal override ICachedChannel GetChannel(ulong id) @@ -94,20 +94,20 @@ namespace Discord.Data return null; } - internal override CachedPublicUser GetUser(ulong id) + internal override CachedGlobalUser GetUser(ulong id) { - CachedPublicUser user; + CachedGlobalUser user; if (_users.TryGetValue(id, out user)) return user; return null; } - internal override CachedPublicUser GetOrAddUser(ulong id, Func userFactory) + internal override CachedGlobalUser GetOrAddUser(ulong id, Func userFactory) { return _users.GetOrAdd(id, userFactory); } - internal override CachedPublicUser RemoveUser(ulong id) + internal override CachedGlobalUser RemoveUser(ulong id) { - CachedPublicUser user; + CachedGlobalUser user; if (_users.TryRemove(id, out user)) return user; return null; diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs index f10507f3c..26d9c6e40 100644 --- a/src/Discord.Net/Data/IDataStore.cs +++ b/src/Discord.Net/Data/IDataStore.cs @@ -8,7 +8,7 @@ namespace Discord.Data internal abstract IReadOnlyCollection Channels { get; } internal abstract IReadOnlyCollection DMChannels { get; } internal abstract IReadOnlyCollection Guilds { get; } - internal abstract IReadOnlyCollection Users { get; } + internal abstract IReadOnlyCollection Users { get; } internal abstract ICachedChannel GetChannel(ulong id); internal abstract void AddChannel(ICachedChannel channel); @@ -22,8 +22,8 @@ namespace Discord.Data internal abstract void AddGuild(CachedGuild guild); internal abstract CachedGuild RemoveGuild(ulong id); - internal abstract CachedPublicUser GetUser(ulong id); - internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func userFactory); - internal abstract CachedPublicUser RemoveUser(ulong id); + internal abstract CachedGlobalUser GetUser(ulong id); + internal abstract CachedGlobalUser GetOrAddUser(ulong userId, Func userFactory); + internal abstract CachedGlobalUser RemoveUser(ulong id); } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 443ba0890..6d26614ee 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -13,7 +13,6 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Diagnostics; namespace Discord { @@ -51,15 +50,17 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; - private ConcurrentHashSet _dmChannels; + private string _sessionId; private int _lastSeq; private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; - private CancellationTokenSource _heartbeatCancelToken; - private Task _heartbeatTask, _reconnectTask; + private CancellationTokenSource _cancelToken; + private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; private long _heartbeatTime; private bool _isReconnecting; + private int _unavailableGuilds; + private long _lastGuildAvailableTime; /// Gets the shard if of this client. public int ShardId { get; } @@ -74,15 +75,7 @@ namespace Discord internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; internal IReadOnlyCollection Guilds => DataStore.Guilds; - internal IReadOnlyCollection DMChannels - { - get - { - var dmChannels = _dmChannels; - var store = DataStore; - return dmChannels.Select(x => store.GetChannel(x) as CachedDMChannel).Where(x => x != null).ToReadOnlyCollection(dmChannels); - } - } + internal IReadOnlyCollection DMChannels => DataStore.DMChannels; internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. @@ -132,7 +125,6 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); - _dmChannels = new ConcurrentHashSet(); } protected override async Task OnLoginAsync() @@ -169,10 +161,11 @@ namespace Discord try { _connectTask = new TaskCompletionSource(); - _heartbeatCancelToken = new CancellationTokenSource(); + _cancelToken = new CancellationTokenSource(); await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); + ConnectionState = ConnectionState.Connected; await _gatewayLogger.InfoAsync("Connected"); } @@ -203,9 +196,24 @@ namespace Discord ConnectionState = ConnectionState.Disconnecting; await _gatewayLogger.InfoAsync("Disconnecting"); - try { _heartbeatCancelToken.Cancel(); } catch { } + //Signal tasks to complete + try { _cancelToken.Cancel(); } catch { } + + //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); - await _heartbeatTask.ConfigureAwait(false); + + //Wait for tasks to complete + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; + + var guildDownloadTask = _guildDownloadTask; + if (guildDownloadTask != null) + await guildDownloadTask.ConfigureAwait(false); + _guildDownloadTask = null; + + //Clear large guild queue while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; @@ -216,22 +224,21 @@ namespace Discord private async Task StartReconnectAsync() { //TODO: Is this thread-safe? - while (true) + await _log.InfoAsync("Debug", "Trying to reconnect...").ConfigureAwait(false); + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { if (_reconnectTask != null) return; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (_reconnectTask != null) return; - _isReconnecting = true; - _reconnectTask = ReconnectInternalAsync(); - } - finally { _connectionLock.Release(); } + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); } + finally { _connectionLock.Release(); } } private async Task ReconnectInternalAsync() { + await _log.InfoAsync("Debug", "Reconnecting...").ConfigureAwait(false); try { int nextReconnectDelay = 1000; @@ -255,13 +262,18 @@ namespace Discord catch (Exception ex) { await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); - } } + } + } } finally { - _isReconnecting = false; - _reconnectTask = null; - + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + _reconnectTask = null; + } + finally { _connectionLock.Release(); } } } @@ -318,20 +330,22 @@ namespace Discord { return Task.FromResult>(DMChannels); } - internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet dmChannels) + internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) { var recipient = GetOrAddUser(model.Recipient.Value, dataStore); - var channel = recipient.AddDMChannel(this, model); - dataStore.AddChannel(channel); - dmChannels.TryAdd(model.Id); + var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model); + recipient.AddRef(); + dataStore.AddDMChannel(channel); return channel; } internal CachedDMChannel RemoveDMChannel(ulong id) { - var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; - var recipient = dmChannel.Recipient; - recipient.RemoveDMChannel(id); - _dmChannels.TryRemove(id); + var dmChannel = DataStore.RemoveDMChannel(id); + if (dmChannel != null) + { + var recipient = dmChannel.Recipient; + recipient.User.RemoveRef(this); + } return dmChannel; } @@ -345,13 +359,13 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } - internal CachedPublicUser GetOrAddUser(API.User model, DataStore dataStore) + internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) { - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(model)); + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); user.AddRef(); return user; } - internal CachedPublicUser RemoveUser(ulong id) + internal CachedGlobalUser RemoveUser(ulong id) { return DataStore.RemoveUser(id); } @@ -425,7 +439,7 @@ namespace Discord else await ApiClient.SendIdentifyAsync().ConfigureAwait(false); _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); } break; case GatewayOpCode.Heartbeat: @@ -439,12 +453,16 @@ namespace Discord { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - var latency = (int)(Environment.TickCount - _heartbeatTime); - _heartbeatTime = 0; - await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); - Latency = latency; + var heartbeatTime = _heartbeatTime; + if (heartbeatTime != 0) + { + var latency = (int)(Environment.TickCount - _heartbeatTime); + _heartbeatTime = 0; + await _gatewayLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + Latency = latency; - await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + } } break; case GatewayOpCode.InvalidSession: @@ -475,21 +493,29 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - var dmChannels = new ConcurrentHashSet(); var currentUser = new CachedSelfUser(this, data.User); + int unavailableGuilds = 0; //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); for (int i = 0; i < data.Guilds.Length; i++) - AddGuild(data.Guilds[i], dataStore); + { + var model = data.Guilds[i]; + AddGuild(model, dataStore); + if (model.Unavailable == true) + unavailableGuilds++; + } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore, dmChannels); + AddDMChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; _currentUser = currentUser; - _dmChannels = dmChannels; + _unavailableGuilds = unavailableGuilds; + _lastGuildAvailableTime = Environment.TickCount; DataStore = dataStore; + _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); + await Ready.RaiseAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete @@ -503,7 +529,10 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); if (data.Unavailable == false) + { type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + } await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); CachedGuild guild; @@ -511,6 +540,7 @@ namespace Discord { guild = AddGuild(data, DataStore); await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); + await _gatewayLogger.InfoAsync($"Joined {data.Name}").ConfigureAwait(false); } else { @@ -526,7 +556,7 @@ namespace Discord if (data.Unavailable != true) { - await _gatewayLogger.InfoAsync($"Connected to {data.Name}").ConfigureAwait(false); + await _gatewayLogger.VerboseAsync($"Connected to {data.Name}").ConfigureAwait(false); await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); } } @@ -564,7 +594,7 @@ namespace Discord member.User.RemoveRef(this); await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); - await _gatewayLogger.InfoAsync($"Disconnected from {data.Name}").ConfigureAwait(false); + await _gatewayLogger.VerboseAsync($"Disconnected from {data.Name}").ConfigureAwait(false); if (data.Unavailable != true) { await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); @@ -587,7 +617,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; - if (data.GuildId.IsSpecified) + if (!data.IsPrivate) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -599,7 +629,7 @@ namespace Discord } } else - channel = AddDMChannel(data, DataStore, _dmChannels); + channel = AddDMChannel(data, DataStore); if (channel != null) await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } @@ -629,7 +659,7 @@ namespace Discord ICachedChannel channel = null; var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + if (!data.IsPrivate) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -975,9 +1005,9 @@ namespace Discord } else { - var user = DataStore.GetUser(data.User.Id); - if (user == null) - user.Update(data, UpdateSource.WebSocket); + var channel = DataStore.GetDMChannel(data.User.Id); + if (channel != null) + channel.Recipient.Update(data, UpdateSource.WebSocket); } } break; @@ -1095,22 +1125,37 @@ namespace Discord { try { - var state = ConnectionState; - while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + while (!cancelToken.IsCancellationRequested) { await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); if (_heartbeatTime != 0) //Server never responded to our last heartbeat { - await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); - return; + if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) + { + await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + return; + } } - _heartbeatTime = Environment.TickCount; + else + _heartbeatTime = Environment.TickCount; await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); } } catch (OperationCanceledException) { } } + + private async Task WaitForGuildsAsync(CancellationToken cancelToken) + { + while ((_unavailableGuilds > 0) || (Environment.TickCount - _lastGuildAvailableTime > 2000)) + await Task.Delay(500, cancelToken).ConfigureAwait(false); + } + public async Task WaitForGuildsAsync() + { + var downloadTask = _guildDownloadTask; + if (downloadTask != null) + await _guildDownloadTask.ConfigureAwait(false); + } } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index 6294e3f21..7df415b49 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -14,11 +14,11 @@ namespace Discord internal class DMChannel : SnowflakeEntity, IDMChannel { public override DiscordClient Discord { get; } - public User Recipient { get; private set; } + public IUser Recipient { get; private set; } public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - public DMChannel(DiscordClient discord, User recipient, Model model) + public DMChannel(DiscordClient discord, IUser recipient, Model model) : base(model.Id) { Discord = discord; @@ -30,7 +30,9 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Recipient.Update(model.Recipient.Value, UpdateSource.Rest); + //TODO: Is this cast okay? + if (Recipient is User) + (Recipient as User).Update(model.Recipient.Value, source); } public async Task UpdateAsync() @@ -119,8 +121,7 @@ namespace Discord public override string ToString() => '@' + Recipient.ToString(); private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - - IUser IDMChannel.Recipient => Recipient; + IMessage IMessageChannel.GetCachedMessage(ulong id) => null; } } diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 7716f897d..9898c6132 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -1,7 +1,5 @@ using Discord.API.Rest; -using Discord.Extensions; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -14,7 +12,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel { - private ConcurrentDictionary _overwrites; + private List _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? public string Name { get; private set; } public int Position { get; private set; } @@ -38,9 +36,9 @@ namespace Discord Position = model.Position.Value; var overwrites = model.PermissionOverwrites.Value; - var newOverwrites = new ConcurrentDictionary(); + var newOverwrites = new List(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) - newOverwrites[overwrites[i].TargetId] = new Overwrite(overwrites[i]); + newOverwrites.Add(new Overwrite(overwrites[i])); _overwrites = newOverwrites; } @@ -89,16 +87,20 @@ namespace Discord public OverwritePermissions? GetPermissionOverwrite(IUser user) { - Overwrite value; - if (_overwrites.TryGetValue(user.Id, out value)) - return value.Permissions; + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } return null; } public OverwritePermissions? GetPermissionOverwrite(IRole role) { - Overwrite value; - if (_overwrites.TryGetValue(role.Id, out value)) - return value.Permissions; + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == role.Id) + return _overwrites[i].Permissions; + } return null; } @@ -106,34 +108,46 @@ namespace Discord { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); - _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); + _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); } public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); - _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); + _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); } public async Task RemovePermissionOverwriteAsync(IUser user) { await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); - Overwrite value; - _overwrites.TryRemove(user.Id, out value); + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == user.Id) + { + _overwrites.RemoveAt(i); + return; + } + } } public async Task RemovePermissionOverwriteAsync(IRole role) { await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); - Overwrite value; - _overwrites.TryRemove(role.Id, out value); + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == role.Id) + { + _overwrites.RemoveAt(i); + return; + } + } } public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; IGuild IGuildChannel.Guild => Guild; - IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); + IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly(); async Task IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); async Task> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index bd5826473..3778f98b3 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -33,8 +33,9 @@ namespace Discord public bool IsBot => User.IsBot; public string Mention => User.Mention; public string Username => User.Username; - public virtual UserStatus Status => User.Status; - public virtual Game Game => User.Game; + + public virtual UserStatus Status => UserStatus.Unknown; + public virtual Game Game => null; public DiscordClient Discord => Guild.Discord; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 3e32bb954..fdfc06abf 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -1,7 +1,5 @@ -using Discord.API.Rest; -using System; +using System; using System.Diagnostics; -using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 568cef3d9..ed3eadac9 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -9,16 +9,19 @@ namespace Discord { internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel { - private readonly MessageCache _messages; + private readonly MessageManager _messages; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; + public new CachedDMUser Recipient => base.Recipient as CachedDMUser; public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); - public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) + public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) : base(discord, recipient, model) { - _messages = new MessageCache(Discord, this); + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + else + _messages = new MessageManager(Discord, this); } public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs new file mode 100644 index 000000000..de69c7c91 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs @@ -0,0 +1,38 @@ +using System; +using PresenceModel = Discord.API.Presence; + +namespace Discord +{ + internal class CachedDMUser : ICachedUser + { + public CachedGlobalUser User { get; } + + public Game Game { get; private set; } + public UserStatus Status { get; private set; } + + public DiscordSocketClient Discord => User.Discord; + + public ulong Id => User.Id; + public string AvatarUrl => User.AvatarUrl; + public DateTimeOffset CreatedAt => User.CreatedAt; + public string Discriminator => User.Discriminator; + public bool IsAttached => User.IsAttached; + public bool IsBot => User.IsBot; + public string Mention => User.Mention; + public string Username => User.Username; + + public CachedDMUser(CachedGlobalUser user) + { + User = user; + } + + public void Update(PresenceModel model, UpdateSource source) + { + Status = model.Status; + Game = model.Game != null ? new Game(model.Game) : null; + } + + public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; + ICachedUser ICachedUser.Clone() => Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs new file mode 100644 index 000000000..e07472ae8 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs @@ -0,0 +1,39 @@ +using System; +using Model = Discord.API.User; + +namespace Discord +{ + internal class CachedGlobalUser : User, ICachedUser + { + private ushort _references; + + public new DiscordSocketClient Discord { get { throw new NotSupportedException(); } } + public override UserStatus Status => UserStatus.Unknown;// _status; + public override Game Game => null; //_game; + + public CachedGlobalUser(Model model) + : base(model) + { + } + + public void AddRef() + { + checked + { + lock (this) + _references++; + } + } + public void RemoveRef(DiscordSocketClient discord) + { + lock (this) + { + if (--_references == 0) + discord.RemoveUser(Id); + } + } + + public CachedGlobalUser Clone() => MemberwiseClone() as CachedGlobalUser; + ICachedUser ICachedUser.Clone() => Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 427ad6699..294752d64 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -10,7 +10,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; - public new CachedPublicUser User => base.User as CachedPublicUser; + public new CachedGlobalUser User => base.User as CachedGlobalUser; public override Game Game => _game; public override UserStatus Status => _status; @@ -21,11 +21,11 @@ namespace Discord public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; - public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) + public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, Model model) : base(guild, user, model) { } - public CachedGuildUser(CachedGuild guild, CachedPublicUser user, PresenceModel model) + public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, PresenceModel model) : base(guild, user, model) { } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs deleted file mode 100644 index 915c897a4..000000000 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ /dev/null @@ -1,75 +0,0 @@ -using ChannelModel = Discord.API.Channel; -using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; - -namespace Discord -{ - internal class CachedPublicUser : User, ICachedUser - { - //TODO: Fix removed game/status (add CachedDMUser?) - private int _references; - //private Game? _game; - //private UserStatus _status; - - public CachedDMChannel DMChannel { get; private set; } - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public override UserStatus Status => UserStatus.Unknown;// _status; - public override Game Game => null; //_game; - - public CachedPublicUser(Model model) - : base(model) - { - } - - public CachedDMChannel AddDMChannel(DiscordSocketClient discord, ChannelModel model) - { - lock (this) - { - var channel = new CachedDMChannel(discord, this, model); - DMChannel = channel; - return channel; - } - } - public CachedDMChannel RemoveDMChannel(ulong id) - { - lock (this) - { - var channel = DMChannel; - if (channel.Id == id) - { - DMChannel = null; - return channel; - } - return null; - } - } - - public void Update(PresenceModel model, UpdateSource source) - { - if (source == UpdateSource.Rest) return; - - //var game = model.Game != null ? new Game(model.Game) : (Game)null; - - //_status = model.Status; - //_game = game; - } - - public void AddRef() - { - lock (this) - _references++; - } - public void RemoveRef(DiscordSocketClient discord) - { - lock (this) - { - if (--_references == 0 && DMChannel == null) - discord.RemoveUser(Id); - } - } - - public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; - ICachedUser ICachedUser.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 410d36e58..7a91a8221 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -9,7 +9,7 @@ namespace Discord { internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel { - private readonly MessageCache _messages; + private readonly MessageManager _messages; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; @@ -20,7 +20,10 @@ namespace Discord public CachedTextChannel(CachedGuild guild, Model model) : base(guild, model) { - _messages = new MessageCache(Discord, this); + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + else + _messages = new MessageManager(Discord, this); } public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index 7aa99bcd2..0eaee13c3 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Extensions; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -9,26 +8,23 @@ using System.Threading.Tasks; namespace Discord { - internal class MessageCache + internal class MessageCache : MessageManager { - private readonly DiscordSocketClient _discord; - private readonly ICachedMessageChannel _channel; private readonly ConcurrentDictionary _messages; private readonly ConcurrentQueue _orderedMessages; private readonly int _size; - public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); + public override IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); public MessageCache(DiscordSocketClient discord, ICachedMessageChannel channel) + : base(discord, channel) { - _discord = discord; - _channel = channel; _size = discord.MessageCacheSize; _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); _orderedMessages = new ConcurrentQueue(); } - public void Add(CachedMessage message) + public override void Add(CachedMessage message) { if (_messages.TryAdd(message.Id, message)) { @@ -41,21 +37,21 @@ namespace Discord } } - public CachedMessage Remove(ulong id) + public override CachedMessage Remove(ulong id) { CachedMessage msg; _messages.TryRemove(id, out msg); return msg; } - public CachedMessage Get(ulong id) + public override CachedMessage Get(ulong id) { CachedMessage result; if (_messages.TryGetValue(id, out result)) return result; return null; } - public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public override IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (limit == 0) return ImmutableArray.Empty; @@ -81,57 +77,12 @@ namespace Discord .ToImmutableArray(); } - public async Task DownloadAsync(ulong id) + public override async Task DownloadAsync(ulong id) { var msg = Get(id); if (msg != null) return msg; - var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); - if (model != null) - return new CachedMessage(_channel, new User(model.Author.Value), model); - return null; - } - public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) - { - //TODO: Test heavily, especially the ordering of messages - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; - - var cachedMessages = GetMany(fromId, dir, limit); - if (cachedMessages.Count == limit) - return cachedMessages; - else if (cachedMessages.Count > limit) - return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); - else - { - Optional relativeId; - if (cachedMessages.Count == 0) - relativeId = fromId ?? new Optional(); - else - relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; - var args = new GetChannelMessagesParams - { - Limit = limit - cachedMessages.Count, - RelativeDirection = dir, - RelativeMessageId = relativeId - }; - var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - - var guild = (_channel as ICachedGuildChannel).Guild; - return cachedMessages.Concat(downloadedMessages.Select(x => - { - IUser user = _channel.GetUser(x.Author.Value.Id, true); - if (user == null) - { - var newUser = new User(x.Author.Value); - if (guild != null) - user = new GuildUser(guild, newUser); - else - user = newUser; - } - return new CachedMessage(_channel, user, x); - })).ToImmutableArray(); - } + return await base.DownloadAsync(id).ConfigureAwait(false); } } } diff --git a/src/Discord.Net/Entities/WebSocket/MessageManager.cs b/src/Discord.Net/Entities/WebSocket/MessageManager.cs new file mode 100644 index 000000000..98fde21b0 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/MessageManager.cs @@ -0,0 +1,81 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + internal class MessageManager + { + private readonly DiscordSocketClient _discord; + private readonly ICachedMessageChannel _channel; + + public virtual IReadOnlyCollection Messages + => ImmutableArray.Create(); + + public MessageManager(DiscordSocketClient discord, ICachedMessageChannel channel) + { + _discord = discord; + _channel = channel; + } + + public virtual void Add(CachedMessage message) { } + public virtual CachedMessage Remove(ulong id) => null; + public virtual CachedMessage Get(ulong id) => null; + + public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => ImmutableArray.Create(); + + public virtual async Task DownloadAsync(ulong id) + { + var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); + if (model != null) + return new CachedMessage(_channel, new User(model.Author.Value), model); + return null; + } + public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) + { + //TODO: Test heavily, especially the ordering of messages + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) return ImmutableArray.Empty; + + var cachedMessages = GetMany(fromId, dir, limit); + if (cachedMessages.Count == limit) + return cachedMessages; + else if (cachedMessages.Count > limit) + return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); + else + { + Optional relativeId; + if (cachedMessages.Count == 0) + relativeId = fromId ?? new Optional(); + else + relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; + var args = new GetChannelMessagesParams + { + Limit = limit - cachedMessages.Count, + RelativeDirection = dir, + RelativeMessageId = relativeId + }; + var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); + + var guild = (_channel as ICachedGuildChannel).Guild; + return cachedMessages.Concat(downloadedMessages.Select(x => + { + IUser user = _channel.GetUser(x.Author.Value.Id, true); + if (user == null) + { + var newUser = new User(x.Author.Value); + if (guild != null) + user = new GuildUser(guild, newUser); + else + user = newUser; + } + return new CachedMessage(_channel, user, x); + })).ToImmutableArray(); + } + } + } +} From ff806d5468d3150d5a1bd153c3b4ec8516ce125f Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:10:17 -0300 Subject: [PATCH 085/160] Dont crash if the websocket's cancelTokenSource.Cancel throws an exception --- .../Net/WebSockets/DefaultWebsocketClient.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs index cb31b095c..d9c518874 100644 --- a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -72,17 +72,13 @@ namespace Discord.Net.WebSockets public async Task DisconnectAsync() { //Assume locked - _cancelTokenSource.Cancel(); + try { _cancelTokenSource.Cancel(false); } catch { } if (_client != null && _client.State == WebSocketState.Open) { - try - { - var task = _client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - if (task != null) - await task.ConfigureAwait(false); - } - catch { } + var task = _client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + if (task != null) + await task.ConfigureAwait(false); } await (_task ?? Task.CompletedTask).ConfigureAwait(false); From 23bd574830fb01bb4c34b248cdf89bac469293a4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:16:54 -0300 Subject: [PATCH 086/160] Fixed WaitForGuildsAsync --- src/Discord.Net/DiscordSocketClient.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 6d26614ee..cfee05de7 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -552,6 +552,10 @@ namespace Discord await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); return; } + + var unavailableGuilds = _unavailableGuilds; + if (unavailableGuilds != 0) + _unavailableGuilds = unavailableGuilds - 1; } if (data.Unavailable != true) @@ -600,6 +604,8 @@ namespace Discord await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Left {data.Name}").ConfigureAwait(false); } + else + _unavailableGuilds++; } else @@ -1148,7 +1154,7 @@ namespace Discord private async Task WaitForGuildsAsync(CancellationToken cancelToken) { - while ((_unavailableGuilds > 0) || (Environment.TickCount - _lastGuildAvailableTime > 2000)) + while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); } public async Task WaitForGuildsAsync() From 4215a9d8cd00c32a33f8a2ffc30ccf5181fd5e80 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:18:56 -0300 Subject: [PATCH 087/160] Fixed removing DMs from cache --- src/Discord.Net/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index cfee05de7..1f27d0f1b 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -677,7 +677,7 @@ namespace Discord } } else - channel = RemoveDMChannel(data.Id); + channel = RemoveDMChannel(data.Recipient.Id); if (channel != null) await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); else From 52053ac9424ee747ad9fef244938a8de0d22fcf8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:16:54 -0300 Subject: [PATCH 088/160] Fixed WaitForGuildsAsync --- src/Discord.Net/DiscordSocketClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 6d26614ee..77e5c37e7 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -552,6 +552,10 @@ namespace Discord await _gatewayLogger.WarningAsync($"{type} referenced an unknown guild.").ConfigureAwait(false); return; } + + var unavailableGuilds = _unavailableGuilds; + if (unavailableGuilds != 0) + _unavailableGuilds = unavailableGuilds - 1; } if (data.Unavailable != true) @@ -600,6 +604,8 @@ namespace Discord await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Left {data.Name}").ConfigureAwait(false); } + else + _unavailableGuilds++; } else @@ -671,7 +677,7 @@ namespace Discord } } else - channel = RemoveDMChannel(data.Id); + channel = RemoveDMChannel(data.Recipient.Value.Id); if (channel != null) await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); else @@ -1148,7 +1154,7 @@ namespace Discord private async Task WaitForGuildsAsync(CancellationToken cancelToken) { - while ((_unavailableGuilds > 0) || (Environment.TickCount - _lastGuildAvailableTime > 2000)) + while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); } public async Task WaitForGuildsAsync() From 4f1623d5e994ae1bd525d59cec6a409f93a8e528 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 02:14:11 -0300 Subject: [PATCH 089/160] Fixed ModifyAsync null checks --- src/Discord.Net/Entities/Channels/GuildChannel.cs | 2 +- src/Discord.Net/Entities/Channels/TextChannel.cs | 2 +- src/Discord.Net/Entities/Channels/VoiceChannel.cs | 2 +- src/Discord.Net/Entities/Users/SelfUser.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 9898c6132..78838d242 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -51,7 +51,7 @@ namespace Discord } public async Task ModifyAsync(Action func) { - if (func != null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyGuildChannelParams(); func(args); diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs index 4b2fe150e..c32b67c74 100644 --- a/src/Discord.Net/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -32,7 +32,7 @@ namespace Discord public async Task ModifyAsync(Action func) { - if (func != null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyTextChannelParams(); func(args); diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index 745762876..4e2cb6e80 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -28,7 +28,7 @@ namespace Discord public async Task ModifyAsync(Action func) { - if (func != null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyVoiceChannelParams(); func(args); diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index fed6f5e58..22fa60386 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -33,7 +33,7 @@ namespace Discord } public async Task ModifyAsync(Action func) { - if (func != null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new NullReferenceException(nameof(func)); var args = new ModifyCurrentUserParams(); func(args); From 7d5c6e10b4d0e4cc3b8fba3282dc82468fc20d78 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 02:17:04 -0300 Subject: [PATCH 090/160] Fixed null discordclient when editing current user --- src/Discord.Net/Entities/Users/SelfUser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 22fa60386..4629e85a0 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -10,9 +10,12 @@ namespace Discord public string Email { get; private set; } public bool IsVerified { get; private set; } + public override DiscordClient Discord { get; } + public SelfUser(DiscordClient discord, Model model) : base(model) { + Discord = discord; } public override void Update(Model model, UpdateSource source) { From 851c60fcbf6b91cc1878e1089a8a6c741c2131c4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 02:50:42 -0300 Subject: [PATCH 091/160] Improved Json.Net contract resolving logic with optionals --- .../Net/Converters/DiscordContractResolver.cs | 108 ++++++++++-------- .../Net/Converters/ImageConverter.cs | 5 +- .../Net/Converters/OptionalConverter.cs | 22 +++- 3 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index be22c9b20..4e3aba41e 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; @@ -17,59 +18,34 @@ namespace Discord.Net.Converters { var property = base.CreateProperty(member, memberSerialization); var propInfo = member as PropertyInfo; - if (propInfo != null) { - JsonConverter converter = null; - var type = property.PropertyType; - var typeInfo = type.GetTypeInfo(); + JsonConverter converter; + var type = propInfo.PropertyType; - //Primitives - if (propInfo.GetCustomAttribute() == null) + if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) { - if (type == typeof(ulong)) - converter = UInt64Converter.Instance; - else if (type == typeof(ulong?)) - converter = NullableUInt64Converter.Instance; - else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEnumerable))) - converter = UInt64ArrayConverter.Instance; - } - if (converter == null) - { - //Enums - if (type == typeof(ChannelType)) - converter = ChannelTypeConverter.Instance; - else if (type == typeof(PermissionTarget)) - converter = PermissionTargetConverter.Instance; - else if (type == typeof(UserStatus)) - converter = UserStatusConverter.Instance; - else if (type == typeof(Direction)) - converter = DirectionConverter.Instance; + var typeInput = propInfo.DeclaringType; + var innerTypeOutput = type.GenericTypeArguments[0]; - //Entities - if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) - converter = UInt64EntityConverter.Instance; - else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) - converter = StringEntityConverter.Instance; + var getter = typeof(Func<,>).MakeGenericType(typeInput, type); + var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); + var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); + property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); - //Special - else if (type == typeof(string) && propInfo.GetCustomAttribute() != null) - converter = ImageConverter.Instance; - else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) + var converterType = typeof(OptionalConverter<>).MakeGenericType(innerTypeOutput).GetTypeInfo(); + var instanceField = converterType.GetDeclaredField("Instance"); + converter = instanceField.GetValue(null) as JsonConverter; + if (converter == null) { - var typeInput = propInfo.DeclaringType; - var innerTypeOutput = type.GenericTypeArguments[0]; - - var getter = typeof(Func<,>).MakeGenericType(typeInput, type); - var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); - var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); - var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); - property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); - - var converterType = typeof(OptionalConverter<>).MakeGenericType(innerTypeOutput); - converter = converterType.GetTypeInfo().GetDeclaredField("Instance").GetValue(null) as JsonConverter; + var innerConverter = GetConverter(propInfo, innerTypeOutput); + converter = converterType.DeclaredConstructors.FirstOrDefault().Invoke(new object[] { innerConverter }) as JsonConverter; + instanceField.SetValue(null, converter); } } + else + converter = GetConverter(propInfo, type); if (converter != null) { @@ -77,10 +53,52 @@ namespace Discord.Net.Converters property.MemberConverter = converter; } } - return property; } + private JsonConverter GetConverter(PropertyInfo propInfo, Type type, TypeInfo typeInfo = null) + { + bool hasInt53 = propInfo.GetCustomAttribute() != null; + + //Primitives + if (!hasInt53) + { + if (type == typeof(ulong)) + return UInt64Converter.Instance; + if (type == typeof(ulong?)) + return NullableUInt64Converter.Instance; + } + + //Enums + if (type == typeof(ChannelType)) + return ChannelTypeConverter.Instance; + if (type == typeof(PermissionTarget)) + return PermissionTargetConverter.Instance; + if (type == typeof(UserStatus)) + return UserStatusConverter.Instance; + if (type == typeof(Direction)) + return DirectionConverter.Instance; + + //Special + if (type == typeof(Stream) && propInfo.GetCustomAttribute() != null) + return ImageConverter.Instance; + + + if (typeInfo == null) typeInfo = type.GetTypeInfo(); + + //Primitives + if (!hasInt53 && typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEnumerable))) + return UInt64ArrayConverter.Instance; + + //Entities + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + return UInt64EntityConverter.Instance; + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + return StringEntityConverter.Instance; + + return null; + } + private static bool ShouldSerialize(object owner, Delegate getter) { return (getter as Func>)((TOwner)owner).IsSpecified; diff --git a/src/Discord.Net/Net/Converters/ImageConverter.cs b/src/Discord.Net/Net/Converters/ImageConverter.cs index a40b5bf86..3446d2b2e 100644 --- a/src/Discord.Net/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net/Net/Converters/ImageConverter.cs @@ -1,5 +1,4 @@ -using Discord.API; -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.IO; @@ -20,8 +19,6 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - if (value is Optional) - value = (Optional)value; var stream = value as Stream; byte[] bytes = new byte[stream.Length - stream.Position]; diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs index a1e7c7543..260d642d4 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -5,20 +5,36 @@ namespace Discord.Net.Converters { public class OptionalConverter : JsonConverter { - public static readonly OptionalConverter Instance = new OptionalConverter(); + public static OptionalConverter Instance; + + private readonly JsonConverter _innerConverter; public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; + public OptionalConverter(JsonConverter innerConverter) + { + _innerConverter = innerConverter; + } + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return new Optional(serializer.Deserialize(reader)); + T obj; + if (_innerConverter != null) + obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + else + obj = serializer.Deserialize(reader); + return new Optional(obj); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - serializer.Serialize(writer, ((Optional)value).Value); + value = ((Optional)value).Value; + if (_innerConverter != null) + _innerConverter.WriteJson(writer, value, serializer); + else + serializer.Serialize(writer, value, typeof(T)); } } } From 19f9abbed953689f892420cd379bfe4c46d7dd95 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 02:50:57 -0300 Subject: [PATCH 092/160] Fixed SelfUser.Modify when no username is provided --- src/Discord.Net/Entities/Users/SelfUser.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 4629e85a0..27ea3f178 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -40,6 +40,10 @@ namespace Discord var args = new ModifyCurrentUserParams(); func(args); + + if (!args.Username.IsSpecified) + args.Username = Username; + var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } From 2e8f67e8a41858af2638412cd0ff5dd48cac6390 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 05:32:26 -0300 Subject: [PATCH 093/160] Merged WaitForGuildsAsync into ConnectAsync --- src/Discord.Net/DiscordSocketClient.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 77e5c37e7..d21687212 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -141,7 +141,7 @@ namespace Discord } /// - public async Task ConnectAsync() + public async Task ConnectAsync(bool waitForGuilds = true) { await _connectionLock.WaitAsync().ConfigureAwait(false); try @@ -150,6 +150,13 @@ namespace Discord await ConnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } + + if (waitForGuilds) + { + var downloadTask = _guildDownloadTask; + if (downloadTask != null) + await _guildDownloadTask.ConfigureAwait(false); + } } private async Task ConnectInternalAsync() { @@ -1157,11 +1164,5 @@ namespace Discord while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); } - public async Task WaitForGuildsAsync() - { - var downloadTask = _guildDownloadTask; - if (downloadTask != null) - await _guildDownloadTask.ConfigureAwait(false); - } } } From 32ab967f4a46619f8a37b57d71fa2997c8d259cc Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 05:32:49 -0300 Subject: [PATCH 094/160] Reorganized commands structure --- .../Attributes/CommandAttribute.cs | 6 +- src/Discord.Net.Commands/Command.cs | 35 ++++++ src/Discord.Net.Commands/CommandMap.cs | 71 ----------- .../{CommandParser.cs => CommandService.cs} | 118 ++++++++++-------- src/Discord.Net.Commands/Module.cs | 33 +++++ src/Discord.Net.Commands/ReflectionUtils.cs | 24 ++++ ...mmandSearchResults.cs => SearchResults.cs} | 4 +- 7 files changed, 164 insertions(+), 127 deletions(-) create mode 100644 src/Discord.Net.Commands/Command.cs delete mode 100644 src/Discord.Net.Commands/CommandMap.cs rename src/Discord.Net.Commands/{CommandParser.cs => CommandService.cs} (51%) create mode 100644 src/Discord.Net.Commands/Module.cs create mode 100644 src/Discord.Net.Commands/ReflectionUtils.cs rename src/Discord.Net.Commands/{CommandSearchResults.cs => SearchResults.cs} (66%) diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index e28017915..db4e877d8 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -5,9 +5,13 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method)] public class CommandAttribute : Attribute { + public string Text { get; } public string Name { get; } - public CommandAttribute(string name) + + public CommandAttribute(string name) : this(name, name) { } + public CommandAttribute(string text, string name) { + Text = text.ToLowerInvariant(); Name = name; } } diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs new file mode 100644 index 000000000..568b645d9 --- /dev/null +++ b/src/Discord.Net.Commands/Command.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; + +namespace Discord.Commands +{ + public class Command + { + private Action _action; + + public string Name { get; } + public string Description { get; } + public string Text { get; } + + internal Command(CommandAttribute attribute, MethodInfo methodInfo) + { + var description = methodInfo.GetCustomAttribute(); + if (description != null) + Description = description.Text; + + Name = attribute.Name; + Text = attribute.Text; + } + + public void Invoke(IMessage msg) + { + _action.Invoke(msg); + } + + private void BuildAction() + { + _action = null; + //TODO: Implement + } + } +} diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs deleted file mode 100644 index f99073189..000000000 --- a/src/Discord.Net.Commands/CommandMap.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; - -namespace Discord.Commands -{ - public class CommandMap - { - private readonly ConcurrentDictionary> _map; - - public CommandMap() - { - _map = new ConcurrentDictionary>(); - } - - public void Add(string key, Command cmd) - { - var list = _map.GetOrAdd(key, _ => new List()); - lock (list) - list.Add(cmd); - } - public void Remove(string key, Command cmd) - { - List list; - if (_map.TryGetValue(key, out list)) - { - lock (list) - list.Remove(cmd); - } - } - public IReadOnlyList Get(string key) - { - List list; - if (_map.TryGetValue(key, out list)) - { - lock (list) - return list.ToImmutableArray(); - } - return ImmutableArray.Create(); - } - - //TODO: C#7 Candidate for tuple - public CommandSearchResults Search(string input) - { - string lowerInput = input.ToLowerInvariant(); - - List bestGroup = null, group; - int startPos = 0, endPos; - - while (true) - { - endPos = input.IndexOf(' ', startPos); - string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); - startPos = endPos + 1; - if (!_map.TryGetValue(cmdText, out group)) - break; - bestGroup = group; - } - - ImmutableArray cmds; - if (bestGroup != null) - { - lock (bestGroup) - cmds = bestGroup.ToImmutableArray(); - } - else - cmds = ImmutableArray.Create(); - return new CommandSearchResults(cmds, startPos); - } - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandService.cs similarity index 51% rename from src/Discord.Net.Commands/CommandParser.cs rename to src/Discord.Net.Commands/CommandService.cs index 8157a3afd..2b9555b73 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Threading; @@ -7,55 +9,22 @@ using System.Threading.Tasks; namespace Discord.Commands { - public class Module - { - public string Name { get; } - public IEnumerable Commands { get; } - - internal Module(object parent, TypeInfo typeInfo) - { - List commands = new List(); - SearchClass(parent, commands, typeInfo); - Commands = commands; - } - - private void SearchClass(object parent, List commands, TypeInfo typeInfo) - { - foreach (var method in typeInfo.DeclaredMethods) - { - if (typeInfo.GetCustomAttribute() != null) - { - - } - } - foreach (var type in typeInfo.DeclaredNestedTypes) - { - if (typeInfo.GetCustomAttribute() != null) - { - SearchClass(CommandParser.CreateObject(typeInfo), commands, type); - } - } - } - } - public class Command - { - public string SourceName { get; } - - internal Command(TypeInfo typeInfo) - { - } - } - - public class CommandParser + public class CommandService { private readonly SemaphoreSlim _moduleLock; - private readonly Dictionary _modules; + private readonly ConcurrentDictionary _modules; + private readonly ConcurrentDictionary> _map; + + public IEnumerable Modules => _modules.Select(x => x.Value); + public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); - public CommandParser() + public CommandService() { - _modules = new Dictionary(); _moduleLock = new SemaphoreSlim(1, 1); + _modules = new ConcurrentDictionary(); + _map = new ConcurrentDictionary>(); } + public async Task Load(object module) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -63,9 +32,11 @@ namespace Discord.Commands { if (_modules.ContainsKey(module)) throw new ArgumentException($"This module has already been loaded."); + var typeInfo = module.GetType().GetTypeInfo(); if (typeInfo.GetCustomAttribute() == null) throw new ArgumentException($"Modules must be marked with ModuleAttribute."); + return LoadInternal(module, typeInfo); } finally @@ -77,6 +48,14 @@ namespace Discord.Commands { var loadedModule = new Module(module, typeInfo); _modules[module] = loadedModule; + + foreach (var cmd in loadedModule.Commands) + { + var list = _map.GetOrAdd(cmd.Text, _ => new List()); + lock (list) + list.Add(cmd); + } + return loadedModule; } public async Task> LoadAssembly(Assembly assembly) @@ -90,7 +69,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.GetCustomAttribute() != null) { - var module = CreateObject(typeInfo); + var module = ReflectionUtils.CreateObject(typeInfo); modules.Add(LoadInternal(module, typeInfo)); } } @@ -107,27 +86,60 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return _modules.Remove(module); + return UnloadInternal(module); } finally { _moduleLock.Release(); } } + private bool UnloadInternal(object module) + { + Module unloadedModule; + if (_modules.TryRemove(module, out unloadedModule)) + { + foreach (var cmd in unloadedModule.Commands) + { + List list; + if (_map.TryGetValue(cmd.Text, out list)) + { + lock (list) + list.Remove(cmd); + } + } + return true; + } + else + return false; + } - internal static object CreateObject(TypeInfo typeInfo) + //TODO: C#7 Candidate for tuple + public SearchResults Search(string input) { - var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); - if (constructor == null) - throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); - try + string lowerInput = input.ToLowerInvariant(); + + List bestGroup = null, group; + int startPos = 0, endPos; + + while (true) { - return constructor.Invoke(null); + endPos = input.IndexOf(' ', startPos); + string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); + startPos = endPos + 1; + if (!_map.TryGetValue(cmdText, out group)) + break; + bestGroup = group; } - catch (Exception ex) + + ImmutableArray cmds; + if (bestGroup != null) { - throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + lock (bestGroup) + cmds = bestGroup.ToImmutableArray(); } + else + cmds = ImmutableArray.Create(); + return new SearchResults(cmds, startPos); } } } diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/Module.cs new file mode 100644 index 000000000..230b3abd7 --- /dev/null +++ b/src/Discord.Net.Commands/Module.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Discord.Commands +{ + public class Module + { + public string Name { get; } + public IEnumerable Commands { get; } + + internal Module(object parent, TypeInfo typeInfo) + { + List commands = new List(); + SearchClass(parent, commands, typeInfo); + Commands = commands; + } + + private void SearchClass(object parent, List commands, TypeInfo typeInfo) + { + foreach (var method in typeInfo.DeclaredMethods) + { + var cmdAttr = method.GetCustomAttribute(); + if (cmdAttr != null) + commands.Add(new Command(cmdAttr, method)); + } + foreach (var type in typeInfo.DeclaredNestedTypes) + { + if (type.GetCustomAttribute() != null) + SearchClass(ReflectionUtils.CreateObject(type), commands, type); + } + } + } +} diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/ReflectionUtils.cs new file mode 100644 index 000000000..28672a06f --- /dev/null +++ b/src/Discord.Net.Commands/ReflectionUtils.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Discord.Commands +{ + internal class ReflectionUtils + { + internal static object CreateObject(TypeInfo typeInfo) + { + var constructor = typeInfo.DeclaredConstructors.Where(x => x.GetParameters().Length == 0).FirstOrDefault(); + if (constructor == null) + throw new InvalidOperationException($"Failed to find a valid constructor for \"{typeInfo.FullName}\""); + try + { + return constructor.Invoke(null); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\"", ex); + } + } + } +} diff --git a/src/Discord.Net.Commands/CommandSearchResults.cs b/src/Discord.Net.Commands/SearchResults.cs similarity index 66% rename from src/Discord.Net.Commands/CommandSearchResults.cs rename to src/Discord.Net.Commands/SearchResults.cs index 4e1cbe025..724b61ecc 100644 --- a/src/Discord.Net.Commands/CommandSearchResults.cs +++ b/src/Discord.Net.Commands/SearchResults.cs @@ -2,12 +2,12 @@ namespace Discord.Commands { - public struct CommandSearchResults + public struct SearchResults { IReadOnlyList Commands { get; } int ArgsPos { get; } - public CommandSearchResults(IReadOnlyList commands, int argsPos) + public SearchResults(IReadOnlyList commands, int argsPos) { Commands = commands; ArgsPos = argsPos; From 3e5193c3b7c7d92fa3716264f23d1ec311e2a4f2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 05:37:07 -0300 Subject: [PATCH 095/160] Added Expressions dependency --- .../Net/Converters/DiscordContractResolver.cs | 16 ++++++---------- src/Discord.Net/project.json | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 4e3aba41e..99d75879f 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Linq.Expressions; namespace Discord.Net.Converters { @@ -28,11 +29,11 @@ namespace Discord.Net.Converters var typeInput = propInfo.DeclaringType; var innerTypeOutput = type.GenericTypeArguments[0]; - var getter = typeof(Func<,>).MakeGenericType(typeInput, type); - var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); - var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); - var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); - property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); + var parentArg = Expression.Parameter(typeof(object)); + var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); + var isSpecified = Expression.Property(optional, "IsSpecified"); + var lambda = Expression.Lambda>(isSpecified, parentArg).Compile(); + property.ShouldSerialize = x => lambda(x); var converterType = typeof(OptionalConverter<>).MakeGenericType(innerTypeOutput).GetTypeInfo(); var instanceField = converterType.GetDeclaredField("Instance"); @@ -98,10 +99,5 @@ namespace Discord.Net.Converters return null; } - - private static bool ShouldSerialize(object owner, Delegate getter) - { - return (getter as Func>)((TOwner)owner).IsSpecified; - } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 7f202690e..b6daffdc4 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -25,6 +25,7 @@ "System.Collections.Immutable": "1.2.0-rc2-24027", "System.IO.Compression": "4.1.0-rc2-24027", "System.IO.FileSystem": "4.0.1-rc2-24027", + "System.Linq.Expressions": "4.0.11-rc2-24027", "System.Net.Http": "4.0.1-rc2-24027", "System.Net.WebSockets.Client": "4.0.0-rc2-24027", "System.Reflection.Extensions": "4.0.1-rc2-24027", From 97078b557a475b13ec440edd6c9d763333f63c42 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 12:19:40 -0300 Subject: [PATCH 096/160] Ensure socket is disconnected before reconnecting --- src/Discord.Net/DiscordSocketClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index d21687212..a2117a846 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -163,6 +163,10 @@ namespace Discord if (LoginState != LoginState.LoggedIn) throw new InvalidOperationException("You must log in before connecting."); + var state = ConnectionState; + if (state == ConnectionState.Connecting || state == ConnectionState.Connected) + await DisconnectInternalAsync().ConfigureAwait(false); + ConnectionState = ConnectionState.Connecting; await _gatewayLogger.InfoAsync("Connecting"); try From a1594d55b38d161c8594ae782658073ec61df088 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 12:24:48 -0300 Subject: [PATCH 097/160] Removed a couple of debug logs --- src/Discord.Net/DiscordSocketClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index a2117a846..95f943d09 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -235,7 +235,6 @@ namespace Discord private async Task StartReconnectAsync() { //TODO: Is this thread-safe? - await _log.InfoAsync("Debug", "Trying to reconnect...").ConfigureAwait(false); if (_reconnectTask != null) return; await _connectionLock.WaitAsync().ConfigureAwait(false); @@ -249,7 +248,6 @@ namespace Discord } private async Task ReconnectInternalAsync() { - await _log.InfoAsync("Debug", "Reconnecting...").ConfigureAwait(false); try { int nextReconnectDelay = 1000; @@ -1138,6 +1136,7 @@ namespace Discord } #endif } + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { try From 7896afdc923be04851aae99acef96539619aa7db Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 16:09:46 -0300 Subject: [PATCH 098/160] Added more user-friendly entity properties to rest params --- src/Discord.Net/API/DiscordAPIClient.cs | 20 ++++++-- .../API/Rest/CreateDMChannelParams.cs | 2 + .../API/Rest/DeleteMessagesParams.cs | 3 ++ .../API/Rest/GetChannelMessagesParams.cs | 1 + .../API/Rest/ModifyGuildEmbedParams.cs | 8 ++-- .../API/Rest/ModifyGuildMemberParams.cs | 14 ++++-- src/Discord.Net/API/Rest/ModifyGuildParams.cs | 17 ++++--- src/Discord.Net/Entities/Users/GuildUser.cs | 8 ++-- .../Net/Converters/DirectionConverter.cs | 47 ------------------- .../Net/Converters/DiscordContractResolver.cs | 2 - src/Discord.Net/Utilities/Optional.cs | 8 +++- 11 files changed, 61 insertions(+), 69 deletions(-) delete mode 100644 src/Discord.Net/Net/Converters/DirectionConverter.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index e7e11f1ca..78396697a 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -535,7 +535,7 @@ namespace Discord.API Preconditions.NotEqual(args.AFKChannelId, 0, nameof(args.AFKChannelId)); Preconditions.AtLeast(args.AFKTimeout, 0, nameof(args.AFKTimeout)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - Preconditions.NotNull(args.Owner, nameof(args.Owner)); + Preconditions.GreaterThan(args.OwnerId, 0, nameof(args.OwnerId)); Preconditions.NotNull(args.Region, nameof(args.Region)); Preconditions.AtLeast(args.VerificationLevel, 0, nameof(args.VerificationLevel)); @@ -831,7 +831,21 @@ namespace Discord.API int limit = args.Limit; ulong? relativeId = args.RelativeMessageId.IsSpecified ? args.RelativeMessageId.Value : (ulong?)null; - string relativeDir = args.RelativeDirection == Direction.After ? "after" : "before"; + string relativeDir; + + switch (args.RelativeDirection) + { + case Direction.Before: + default: + relativeDir = "before"; + break; + case Direction.After: + relativeDir = "after"; + break; + case Direction.Around: + relativeDir = "around"; + break; + } int runs = (limit + DiscordConfig.MaxMessagesPerBatch - 1) / DiscordConfig.MaxMessagesPerBatch; int lastRunCount = limit - (runs - 1) * DiscordConfig.MaxMessagesPerBatch; @@ -1079,7 +1093,7 @@ namespace Discord.API public async Task CreateDMChannelAsync(CreateDMChannelParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.RecipientId, 0, nameof(args.RecipientId)); + Preconditions.GreaterThan(args.RecipientId, 0, nameof(args.Recipient)); return await SendAsync("POST", $"users/@me/channels", args, options: options).ConfigureAwait(false); } diff --git a/src/Discord.Net/API/Rest/CreateDMChannelParams.cs b/src/Discord.Net/API/Rest/CreateDMChannelParams.cs index 9ce033783..779fd5dc9 100644 --- a/src/Discord.Net/API/Rest/CreateDMChannelParams.cs +++ b/src/Discord.Net/API/Rest/CreateDMChannelParams.cs @@ -6,5 +6,7 @@ namespace Discord.API.Rest { [JsonProperty("recipient_id")] public ulong RecipientId { get; set; } + [JsonIgnore] + public IUser Recipient { set { RecipientId = value.Id; } } } } diff --git a/src/Discord.Net/API/Rest/DeleteMessagesParams.cs b/src/Discord.Net/API/Rest/DeleteMessagesParams.cs index 41c6512e4..1ea2fca2a 100644 --- a/src/Discord.Net/API/Rest/DeleteMessagesParams.cs +++ b/src/Discord.Net/API/Rest/DeleteMessagesParams.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Discord.API.Rest { @@ -7,5 +8,7 @@ namespace Discord.API.Rest { [JsonProperty("messages")] public IEnumerable MessageIds { get; set; } + [JsonIgnore] + public IEnumerable Messages { set { MessageIds = value.Select(x => x.Id); } } } } diff --git a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs index c14d1c65f..18107807e 100644 --- a/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs +++ b/src/Discord.Net/API/Rest/GetChannelMessagesParams.cs @@ -6,5 +6,6 @@ public Direction RelativeDirection { get; set; } = Direction.Before; public Optional RelativeMessageId { get; set; } + public Optional RelativeMessage { set { RelativeMessageId = value.IsSpecified ? value.Value.Id : Optional.Create(); } } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs b/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs index f717b4d52..f8e8de1f1 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildEmbedParams.cs @@ -1,5 +1,4 @@ -using Discord.Net.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API.Rest { @@ -7,7 +6,10 @@ namespace Discord.API.Rest { [JsonProperty("enabled")] public Optional Enabled { get; set; } + [JsonProperty("channel")] - public Optional Channel { get; set; } + public Optional ChannelId { get; set; } + [JsonIgnore] + public Optional Channel { set { ChannelId = value.IsSpecified ? value.Value.Id : Optional.Create(); } } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs index 0fbaa6d15..8a4077e90 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildMemberParams.cs @@ -1,18 +1,26 @@ using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; namespace Discord.API.Rest { public class ModifyGuildMemberParams { - [JsonProperty("roles")] - public Optional Roles { get; set; } [JsonProperty("mute")] public Optional Mute { get; set; } [JsonProperty("deaf")] public Optional Deaf { get; set; } [JsonProperty("nick")] public Optional Nickname { get; set; } + + [JsonProperty("roles")] + public Optional> RoleIds { get; set; } + [JsonIgnore] + public Optional> Roles { set { RoleIds = value.IsSpecified ? Optional.Create(value.Value.Select(x => x.Id)) : Optional.Create>(); } } + [JsonProperty("channel_id")] - public Optional VoiceChannel { get; set; } + public Optional VoiceChannelId { get; set; } + [JsonIgnore] + public Optional VoiceChannel { set { VoiceChannelId = value.IsSpecified ? value.Value.Id : Optional.Create(); } } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildParams.cs b/src/Discord.Net/API/Rest/ModifyGuildParams.cs index 6e7ff2e34..d610ab37e 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildParams.cs @@ -1,5 +1,4 @@ -using Discord.Net.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.IO; namespace Discord.API.Rest @@ -12,15 +11,21 @@ namespace Discord.API.Rest public Optional Region { get; set; } [JsonProperty("verification_level")] public Optional VerificationLevel { get; set; } - [JsonProperty("afk_channel_id")] - public Optional AFKChannelId { get; set; } [JsonProperty("afk_timeout")] public Optional AFKTimeout { get; set; } [JsonProperty("icon"), Image] public Optional Icon { get; set; } - [JsonProperty("owner_id")] - public Optional Owner { get; set; } [JsonProperty("splash"), Image] public Optional Splash { get; set; } + + [JsonProperty("afk_channel_id")] + public Optional AFKChannelId { get; set; } + [JsonIgnore] + public Optional AFKChannel { set { OwnerId = value.IsSpecified ? value.Value.Id : Optional.Create(); } } + + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } + [JsonIgnore] + public Optional Owner { set { OwnerId = value.IsSpecified ? value.Value.Id : Optional.Create(); } } } } diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 3778f98b3..0d4784417 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -124,7 +124,7 @@ namespace Discord args.Nickname = new Optional(); //Remove } - if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) + if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.RoleIds.IsSpecified) { await Discord.ApiClient.ModifyGuildMemberAsync(Guild.Id, Id, args).ConfigureAwait(false); if (args.Deaf.IsSpecified) @@ -133,8 +133,8 @@ namespace Discord IsMute = args.Mute.Value; if (args.Nickname.IsSpecified) Nickname = args.Nickname.Value ?? ""; - if (args.Roles.IsSpecified) - Roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); + if (args.RoleIds.IsSpecified) + Roles = args.RoleIds.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } } public async Task KickAsync() @@ -153,7 +153,7 @@ namespace Discord public async Task CreateDMChannelAsync() { - var args = new CreateDMChannelParams { RecipientId = Id }; + var args = new CreateDMChannelParams { Recipient = this }; var model = await Discord.ApiClient.CreateDMChannelAsync(args).ConfigureAwait(false); return new DMChannel(Discord, User, model); diff --git a/src/Discord.Net/Net/Converters/DirectionConverter.cs b/src/Discord.Net/Net/Converters/DirectionConverter.cs deleted file mode 100644 index 899bd880c..000000000 --- a/src/Discord.Net/Net/Converters/DirectionConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.Net.Converters -{ - public class DirectionConverter : JsonConverter - { - public static readonly DirectionConverter Instance = new DirectionConverter(); - - public override bool CanConvert(Type objectType) => true; - public override bool CanRead => true; - public override bool CanWrite => true; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - switch ((string)reader.Value) - { - case "before": - return Direction.Before; - case "after": - return Direction.After; - case "around": - return Direction.Around; - default: - throw new JsonSerializationException("Unknown direction"); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - switch ((Direction)value) - { - case Direction.Before: - writer.WriteValue("before"); - break; - case Direction.After: - writer.WriteValue("after"); - break; - case Direction.Around: - writer.WriteValue("around"); - break; - default: - throw new JsonSerializationException("Invalid direction"); - } - } - } -} diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 99d75879f..f92de2a1c 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -77,8 +77,6 @@ namespace Discord.Net.Converters return PermissionTargetConverter.Instance; if (type == typeof(UserStatus)) return UserStatusConverter.Instance; - if (type == typeof(Direction)) - return DirectionConverter.Instance; //Special if (type == typeof(Stream) && propInfo.GetCustomAttribute() != null) diff --git a/src/Discord.Net/Utilities/Optional.cs b/src/Discord.Net/Utilities/Optional.cs index 8512b28bc..e2d55cf7f 100644 --- a/src/Discord.Net/Utilities/Optional.cs +++ b/src/Discord.Net/Utilities/Optional.cs @@ -7,6 +7,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Optional { + public static Optional Unspecified => default(Optional); private readonly T _value; /// Gets the value for this paramter. @@ -28,7 +29,7 @@ namespace Discord _value = value; IsSpecified = true; } - + public T GetValueOrDefault() => _value; public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : defaultValue; @@ -46,4 +47,9 @@ namespace Discord public static implicit operator Optional(T value) => new Optional(value); public static explicit operator T(Optional value) => value.Value; } + public static class Optional + { + public static Optional Create() => Optional.Unspecified; + public static Optional Create(T value) => new Optional(value); + } } From 6545509adec4d27c09f2b9bbff5b38da32471e70 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 16:10:19 -0300 Subject: [PATCH 099/160] Readded 502 handling --- src/Discord.Net/Net/Queue/RequestQueueBucket.cs | 5 +++++ src/Discord.Net/Net/Rest/DefaultRestClient.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs index bfc7e2bb2..08bd1a388 100644 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -1,6 +1,7 @@ #pragma warning disable CS4014 using System; using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; @@ -68,6 +69,10 @@ namespace Discord.Net.Queue //We have all our semaphores, send the request return await request.SendAsync().ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.BadGateway) + { + continue; + } catch (HttpRateLimitException ex) { Pause(ex.RetryAfterMilliseconds); diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 3b133e615..43b405966 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -127,7 +127,8 @@ namespace Discord.Net.Rest if (statusCode == 429) { //TODO: Include bucket info - throw new HttpRateLimitException(int.Parse(response.Headers.GetValues("retry-after").First())); + int retryAfterMillis = int.Parse(response.Headers.GetValues("retry-after").First()); + throw new HttpRateLimitException(retryAfterMillis); } string reason = null; From e5b00fd157f73dca0afc311f103c0dbb6b3f3765 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 17:34:32 -0300 Subject: [PATCH 100/160] Revert "Added Expressions dependency" This reverts commit 3e5193c3b7c7d92fa3716264f23d1ec311e2a4f2. --- .../Net/Converters/DiscordContractResolver.cs | 16 ++++++++++------ src/Discord.Net/project.json | 1 - 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index f92de2a1c..89eea4f87 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Linq.Expressions; namespace Discord.Net.Converters { @@ -29,11 +28,11 @@ namespace Discord.Net.Converters var typeInput = propInfo.DeclaringType; var innerTypeOutput = type.GenericTypeArguments[0]; - var parentArg = Expression.Parameter(typeof(object)); - var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); - var isSpecified = Expression.Property(optional, "IsSpecified"); - var lambda = Expression.Lambda>(isSpecified, parentArg).Compile(); - property.ShouldSerialize = x => lambda(x); + var getter = typeof(Func<,>).MakeGenericType(typeInput, type); + var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); + var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, innerTypeOutput); + var shouldSerializeDelegate = (Func)shouldSerialize.CreateDelegate(typeof(Func)); + property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); var converterType = typeof(OptionalConverter<>).MakeGenericType(innerTypeOutput).GetTypeInfo(); var instanceField = converterType.GetDeclaredField("Instance"); @@ -97,5 +96,10 @@ namespace Discord.Net.Converters return null; } + + private static bool ShouldSerialize(object owner, Delegate getter) + { + return (getter as Func>)((TOwner)owner).IsSpecified; + } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index b6daffdc4..7f202690e 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -25,7 +25,6 @@ "System.Collections.Immutable": "1.2.0-rc2-24027", "System.IO.Compression": "4.1.0-rc2-24027", "System.IO.FileSystem": "4.0.1-rc2-24027", - "System.Linq.Expressions": "4.0.11-rc2-24027", "System.Net.Http": "4.0.1-rc2-24027", "System.Net.WebSockets.Client": "4.0.0-rc2-24027", "System.Reflection.Extensions": "4.0.1-rc2-24027", From c9171619d91a1abda7bf0f36e65b3894b0df827d Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 19:35:49 -0300 Subject: [PATCH 101/160] Added Ids to rate limit, improved bucket logic, added channel bucks and buckettargets, and exposed bucket data. --- src/Discord.Net/DiscordClient.cs | 12 ++- src/Discord.Net/DiscordSocketClient.cs | 8 +- src/Discord.Net/Net/Queue/BucketDefinition.cs | 16 ---- .../Net/Queue/Definitions/BucketDefinition.cs | 30 ++++++ .../Queue/{ => Definitions}/BucketGroup.cs | 3 +- .../Net/Queue/Definitions/BucketTarget.cs | 9 ++ .../Net/Queue/Definitions/ChannelBucket.cs | 7 ++ .../Queue/{ => Definitions}/GlobalBucket.cs | 0 .../Queue/{ => Definitions}/GuildBucket.cs | 0 src/Discord.Net/Net/Queue/RequestQueue.cs | 96 ++++++++++++++----- .../Net/Queue/RequestQueueBucket.cs | 62 +++++++++--- src/Discord.Net/Net/RateLimitException.cs | 1 + 12 files changed, 183 insertions(+), 61 deletions(-) delete mode 100644 src/Discord.Net/Net/Queue/BucketDefinition.cs create mode 100644 src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs rename src/Discord.Net/Net/Queue/{ => Definitions}/BucketGroup.cs (74%) create mode 100644 src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs create mode 100644 src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs rename src/Discord.Net/Net/Queue/{ => Definitions}/GlobalBucket.cs (100%) rename src/Discord.Net/Net/Queue/{ => Definitions}/GuildBucket.cs (100%) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 001c117e4..eafc06810 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -18,7 +18,7 @@ namespace Discord public event Func Log; public event Func LoggedIn, LoggedOut; - internal readonly Logger _discordLogger, _restLogger; + internal readonly Logger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; internal readonly LogManager _log; internal readonly RequestQueue _requestQueue; @@ -38,12 +38,20 @@ namespace Discord _log.Message += async msg => await Log.RaiseAsync(msg).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); _restLogger = _log.CreateLogger("Rest"); + _queueLogger = _log.CreateLogger("Queue"); _connectionLock = new SemaphoreSlim(1, 1); + _requestQueue = new RequestQueue(); + _requestQueue.RateLimitTriggered += async (id, bucket, millis) => + { + await _queueLogger.WarningAsync($"Rate limit triggered (id = \"{id ?? "null"}\")").ConfigureAwait(false); + if (bucket == null && id != null) + await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); + }; ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); - ApiClient.SentRequest += async (method, endpoint, millis) => await _log.VerboseAsync("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } /// diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 95f943d09..33aa1a105 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -21,8 +21,7 @@ namespace Discord //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { - public event Func Connected, Disconnected; - public event Func Ready; + public event Func Connected, Disconnected, Ready; //public event Func VoiceConnected, VoiceDisconnected; public event Func ChannelCreated, ChannelDestroyed; public event Func ChannelUpdated; @@ -174,6 +173,7 @@ namespace Discord _connectTask = new TaskCompletionSource(); _cancelToken = new CancellationTokenSource(); await ApiClient.ConnectAsync().ConfigureAwait(false); + await Connected.RaiseAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -185,8 +185,6 @@ namespace Discord await DisconnectInternalAsync().ConfigureAwait(false); throw; } - - await Connected.RaiseAsync().ConfigureAwait(false); } /// public async Task DisconnectAsync() @@ -1139,6 +1137,7 @@ namespace Discord private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { + //Clean this up when Discord's session patch is live try { while (!cancelToken.IsCancellationRequested) @@ -1161,7 +1160,6 @@ namespace Discord } catch (OperationCanceledException) { } } - private async Task WaitForGuildsAsync(CancellationToken cancelToken) { while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) diff --git a/src/Discord.Net/Net/Queue/BucketDefinition.cs b/src/Discord.Net/Net/Queue/BucketDefinition.cs deleted file mode 100644 index 64292213d..000000000 --- a/src/Discord.Net/Net/Queue/BucketDefinition.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Discord.Net.Queue -{ - internal struct BucketDefinition - { - public int WindowCount { get; } - public int WindowSeconds { get; } - public GlobalBucket? Parent { get; } - - public BucketDefinition(int windowCount, int windowSeconds, GlobalBucket? parent = null) - { - WindowCount = windowCount; - WindowSeconds = windowSeconds; - Parent = parent; - } - } -} diff --git a/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs b/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs new file mode 100644 index 000000000..cfc53b0c8 --- /dev/null +++ b/src/Discord.Net/Net/Queue/Definitions/BucketDefinition.cs @@ -0,0 +1,30 @@ +namespace Discord.Net.Queue +{ + public sealed class Bucket + { + /// Gets the unique identifier for this bucket. + public string Id { get; } + /// Gets the name of this bucket. + public string Name { get; } + /// Gets the amount of requests that may be sent per window. + public int WindowCount { get; } + /// Gets the length of this bucket's window, in seconds. + public int WindowSeconds { get; } + /// Gets the type of account this bucket affects. + public BucketTarget Target { get; } + /// Gets this bucket's parent. + public GlobalBucket? Parent { get; } + + internal Bucket(string id, int windowCount, int windowSeconds, BucketTarget target, GlobalBucket? parent = null) + : this(id, id, windowCount, windowSeconds, target, parent) { } + internal Bucket(string id, string name, int windowCount, int windowSeconds, BucketTarget target, GlobalBucket? parent = null) + { + Id = id; + Name = name; + WindowCount = windowCount; + WindowSeconds = windowSeconds; + Target = target; + Parent = parent; + } + } +} diff --git a/src/Discord.Net/Net/Queue/BucketGroup.cs b/src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs similarity index 74% rename from src/Discord.Net/Net/Queue/BucketGroup.cs rename to src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs index 0b0367065..e7b0a4181 100644 --- a/src/Discord.Net/Net/Queue/BucketGroup.cs +++ b/src/Discord.Net/Net/Queue/Definitions/BucketGroup.cs @@ -3,6 +3,7 @@ public enum BucketGroup { Global, - Guild + Guild, + Channel } } diff --git a/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs b/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs new file mode 100644 index 000000000..0e5a5d552 --- /dev/null +++ b/src/Discord.Net/Net/Queue/Definitions/BucketTarget.cs @@ -0,0 +1,9 @@ +namespace Discord.Net.Queue +{ + public enum BucketTarget + { + Client, + Bot, + Both + } +} diff --git a/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs b/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs new file mode 100644 index 000000000..235e6dfdf --- /dev/null +++ b/src/Discord.Net/Net/Queue/Definitions/ChannelBucket.cs @@ -0,0 +1,7 @@ +namespace Discord.Net.Queue +{ + public enum ChannelBucket + { + SendEditMessage, + } +} diff --git a/src/Discord.Net/Net/Queue/GlobalBucket.cs b/src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs similarity index 100% rename from src/Discord.Net/Net/Queue/GlobalBucket.cs rename to src/Discord.Net/Net/Queue/Definitions/GlobalBucket.cs diff --git a/src/Discord.Net/Net/Queue/GuildBucket.cs b/src/Discord.Net/Net/Queue/Definitions/GuildBucket.cs similarity index 100% rename from src/Discord.Net/Net/Queue/GuildBucket.cs rename to src/Discord.Net/Net/Queue/Definitions/GuildBucket.cs diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index 27b11a38d..f23176411 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -10,52 +10,83 @@ namespace Discord.Net.Queue { public class RequestQueue { - private readonly static ImmutableDictionary _globalLimits; - private readonly static ImmutableDictionary _guildLimits; + public event Func RateLimitTriggered; + + private readonly static ImmutableDictionary _globalLimits; + private readonly static ImmutableDictionary _guildLimits; + private readonly static ImmutableDictionary _channelLimits; private readonly SemaphoreSlim _lock; private readonly RequestQueueBucket[] _globalBuckets; private readonly ConcurrentDictionary[] _guildBuckets; + private readonly ConcurrentDictionary[] _channelBuckets; private CancellationTokenSource _clearToken; private CancellationToken _parentToken; private CancellationToken _cancelToken; static RequestQueue() { - _globalLimits = new Dictionary + _globalLimits = new Dictionary { //REST - [GlobalBucket.GeneralRest] = new BucketDefinition(0, 0), //No Limit + [GlobalBucket.GeneralRest] = new Bucket(null, "rest", 0, 0, BucketTarget.Both), //No Limit //[GlobalBucket.Login] = new BucketDefinition(1, 1), - [GlobalBucket.DirectMessage] = new BucketDefinition(5, 5), - [GlobalBucket.SendEditMessage] = new BucketDefinition(50, 10), + [GlobalBucket.DirectMessage] = new Bucket("bot:msg:dm", 5, 5, BucketTarget.Bot), + [GlobalBucket.SendEditMessage] = new Bucket("bot:msg:global", 50, 10, BucketTarget.Bot), //Gateway - [GlobalBucket.GeneralGateway] = new BucketDefinition(120, 60), - [GlobalBucket.UpdateStatus] = new BucketDefinition(5, 1, GlobalBucket.GeneralGateway) + [GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both), + [GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway) + }.ToImmutableDictionary(); + + _guildLimits = new Dictionary + { + //REST + [GuildBucket.SendEditMessage] = new Bucket("bot:msg:server", 5, 5, BucketTarget.Bot, GlobalBucket.SendEditMessage), + [GuildBucket.DeleteMessage] = new Bucket("dmsg", 5, 1, BucketTarget.Bot), + [GuildBucket.DeleteMessages] = new Bucket("bdmsg", 1, 1, BucketTarget.Bot), + [GuildBucket.ModifyMember] = new Bucket("guild_member", 10, 10, BucketTarget.Bot), + [GuildBucket.Nickname] = new Bucket("guild_member_nick", 1, 1, BucketTarget.Bot) }.ToImmutableDictionary(); - _guildLimits = new Dictionary + //Client-Only + _channelLimits = new Dictionary { //REST - [GuildBucket.SendEditMessage] = new BucketDefinition(5, 5, GlobalBucket.SendEditMessage), - [GuildBucket.DeleteMessage] = new BucketDefinition(5, 1), - [GuildBucket.DeleteMessages] = new BucketDefinition(1, 1), - [GuildBucket.ModifyMember] = new BucketDefinition(10, 10), - [GuildBucket.Nickname] = new BucketDefinition(1, 1) + [ChannelBucket.SendEditMessage] = new Bucket("msg", 10, 10, BucketTarget.Client, GlobalBucket.SendEditMessage), }.ToImmutableDictionary(); } + public static Bucket GetBucketInfo(GlobalBucket bucket) => _globalLimits[bucket]; + public static Bucket GetBucketInfo(GuildBucket bucket) => _guildLimits[bucket]; + public static Bucket GetBucketInfo(ChannelBucket bucket) => _channelLimits[bucket]; + public RequestQueue() { _lock = new SemaphoreSlim(1, 1); _globalBuckets = new RequestQueueBucket[_globalLimits.Count]; foreach (var pair in _globalLimits) - _globalBuckets[(int)pair.Key] = CreateBucket(pair.Value); + { + //var target = _globalLimits[pair.Key].Target; + //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) + _globalBuckets[(int)pair.Key] = CreateBucket(pair.Value); + } _guildBuckets = new ConcurrentDictionary[_guildLimits.Count]; for (int i = 0; i < _guildLimits.Count; i++) - _guildBuckets[i] = new ConcurrentDictionary(); + { + //var target = _guildLimits[(GuildBucket)i].Target; + //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) + _guildBuckets[i] = new ConcurrentDictionary(); + } + + _channelBuckets = new ConcurrentDictionary[_channelLimits.Count]; + for (int i = 0; i < _channelLimits.Count; i++) + { + //var target = _channelLimits[(GuildBucket)i].Target; + //if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot)) + _channelBuckets[i] = new ConcurrentDictionary(); + } _clearToken = new CancellationTokenSource(); _cancelToken = CancellationToken.None; @@ -72,23 +103,23 @@ namespace Discord.Net.Queue finally { _lock.Release(); } } - internal async Task SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong objId) { request.CancelToken = _cancelToken; - var bucket = GetBucket(group, bucketId, guildId); + var bucket = GetBucket(group, bucketId, objId); return await bucket.SendAsync(request).ConfigureAwait(false); } - internal async Task SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) + internal async Task SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong objId) { request.CancelToken = _cancelToken; - var bucket = GetBucket(group, bucketId, guildId); + var bucket = GetBucket(group, bucketId, objId); return await bucket.SendAsync(request).ConfigureAwait(false); } - private RequestQueueBucket CreateBucket(BucketDefinition def) + private RequestQueueBucket CreateBucket(Bucket def) { var parent = def.Parent != null ? GetGlobalBucket(def.Parent.Value) : null; - return new RequestQueueBucket(def.WindowCount, def.WindowSeconds * 1000, parent); + return new RequestQueueBucket(this, def, parent); } public void DestroyGuildBucket(GuildBucket type, ulong guildId) @@ -97,15 +128,23 @@ namespace Discord.Net.Queue RequestQueueBucket bucket; _guildBuckets[(int)type].TryRemove(guildId, out bucket); } + public void DestroyChannelBucket(ChannelBucket type, ulong channelId) + { + //Assume this object is locked + RequestQueueBucket bucket; + _channelBuckets[(int)type].TryRemove(channelId, out bucket); + } - private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong guildId) + private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong objId) { switch (group) { case BucketGroup.Global: return GetGlobalBucket((GlobalBucket)bucketId); case BucketGroup.Guild: - return GetGuildBucket((GuildBucket)bucketId, guildId); + return GetGuildBucket((GuildBucket)bucketId, objId); + case BucketGroup.Channel: + return GetChannelBucket((ChannelBucket)bucketId, objId); default: throw new ArgumentException($"Unknown bucket group: {group}", nameof(group)); } @@ -118,6 +157,10 @@ namespace Discord.Net.Queue { return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type])); } + private RequestQueueBucket GetChannelBucket(ChannelBucket type, ulong channelId) + { + return _channelBuckets[(int)type].GetOrAdd(channelId, _ => CreateBucket(_channelLimits[type])); + } public async Task ClearAsync() { @@ -133,5 +176,10 @@ namespace Discord.Net.Queue } finally { _lock.Release(); } } + + internal async Task RaiseRateLimitTriggered(string id, Bucket bucket, int millis) + { + await RateLimitTriggered.Invoke(id, bucket, millis).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs index 08bd1a388..a61d71476 100644 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -9,27 +9,51 @@ namespace Discord.Net.Queue { internal class RequestQueueBucket { - private readonly int _windowMilliseconds; + private readonly RequestQueue _queue; private readonly SemaphoreSlim _semaphore; private readonly object _pauseLock; private int _pauseEndTick; private TaskCompletionSource _resumeNotifier; + public Bucket Definition { get; } public RequestQueueBucket Parent { get; } public Task _resetTask { get; } - public RequestQueueBucket(int windowCount, int windowMilliseconds, RequestQueueBucket parent = null) + public RequestQueueBucket(RequestQueue queue, Bucket definition, RequestQueueBucket parent = null) { - if (windowCount != 0) - _semaphore = new SemaphoreSlim(windowCount, windowCount); + _queue = queue; + Definition = definition; + if (definition.WindowCount != 0) + _semaphore = new SemaphoreSlim(definition.WindowCount, definition.WindowCount); + Parent = parent; + _pauseLock = new object(); _resumeNotifier = new TaskCompletionSource(); _resumeNotifier.SetResult(0); - _windowMilliseconds = windowMilliseconds; - Parent = parent; } public async Task SendAsync(IQueuedRequest request) + { + while (true) + { + try + { + return await SendAsyncInternal(request).ConfigureAwait(false); + } + catch (HttpRateLimitException ex) + { + //When a 429 occurs, we drop all our locks, including the ones we wanted. + //This is generally safe though since 429s actually occuring should be very rare. + RequestQueueBucket bucket; + bool success = FindBucket(ex.BucketId, out bucket); + + await _queue.RaiseRateLimitTriggered(ex.BucketId, success ? bucket.Definition : (Bucket)null, ex.RetryAfterMilliseconds).ConfigureAwait(false); + + bucket.Pause(ex.RetryAfterMilliseconds); + } + } + } + private async Task SendAsyncInternal(IQueuedRequest request) { var endTick = request.TimeoutTick; @@ -64,7 +88,7 @@ namespace Discord.Net.Queue { //If there's a parent bucket, pass this request to them if (Parent != null) - return await Parent.SendAsync(request).ConfigureAwait(false); + return await Parent.SendAsyncInternal(request).ConfigureAwait(false); //We have all our semaphores, send the request return await request.SendAsync().ConfigureAwait(false); @@ -73,11 +97,6 @@ namespace Discord.Net.Queue { continue; } - catch (HttpRateLimitException ex) - { - Pause(ex.RetryAfterMilliseconds); - continue; - } } } finally @@ -88,6 +107,23 @@ namespace Discord.Net.Queue } } + private bool FindBucket(string id, out RequestQueueBucket bucket) + { + //Keep going up until we find a bucket with matching id or we're at the topmost bucket + if (Definition.Id == id) + { + bucket = this; + return true; + } + else if (Parent == null) + { + bucket = this; + return false; + } + else + return Parent.FindBucket(id, out bucket); + } + private void Pause(int milliseconds) { lock (_pauseLock) @@ -120,7 +156,7 @@ namespace Discord.Net.Queue } private async Task QueueExitAsync() { - await Task.Delay(_windowMilliseconds).ConfigureAwait(false); + await Task.Delay(Definition.WindowSeconds * 1000).ConfigureAwait(false); _semaphore.Release(); } } diff --git a/src/Discord.Net/Net/RateLimitException.cs b/src/Discord.Net/Net/RateLimitException.cs index a07e90760..4fa4900e9 100644 --- a/src/Discord.Net/Net/RateLimitException.cs +++ b/src/Discord.Net/Net/RateLimitException.cs @@ -4,6 +4,7 @@ namespace Discord.Net { public class HttpRateLimitException : HttpException { + public string BucketId { get; } public int RetryAfterMilliseconds { get; } public HttpRateLimitException(int retryAfterMilliseconds) From 741f10d9b1715ea33e704c33615e6d88ad44cbb0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 20:06:58 -0300 Subject: [PATCH 102/160] Improved 429 handling --- .../Net/Queue/RequestQueueBucket.cs | 12 +++---- src/Discord.Net/Net/RateLimitException.cs | 5 +-- src/Discord.Net/Net/Rest/DefaultRestClient.cs | 35 +++++++++++-------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs index a61d71476..3c914315d 100644 --- a/src/Discord.Net/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -47,7 +47,7 @@ namespace Discord.Net.Queue RequestQueueBucket bucket; bool success = FindBucket(ex.BucketId, out bucket); - await _queue.RaiseRateLimitTriggered(ex.BucketId, success ? bucket.Definition : (Bucket)null, ex.RetryAfterMilliseconds).ConfigureAwait(false); + await _queue.RaiseRateLimitTriggered(ex.BucketId, success ? bucket.Definition : null, ex.RetryAfterMilliseconds).ConfigureAwait(false); bucket.Pause(ex.RetryAfterMilliseconds); } @@ -67,6 +67,7 @@ namespace Discord.Net.Queue //Get our 429 state Task notifier; int resumeTime; + lock (_pauseLock) { notifier = _resumeNotifier.Task; @@ -133,14 +134,14 @@ namespace Discord.Net.Queue { _resumeNotifier = new TaskCompletionSource(); _pauseEndTick = unchecked(Environment.TickCount + milliseconds); - QueueResumeAsync(milliseconds); + QueueResumeAsync(_resumeNotifier, milliseconds); } } } - private async Task QueueResumeAsync(int millis) + private async Task QueueResumeAsync(TaskCompletionSource resumeNotifier, int millis) { await Task.Delay(millis).ConfigureAwait(false); - _resumeNotifier.SetResult(0); + resumeNotifier.SetResult(0); } private async Task EnterAsync(int? endTick) @@ -151,8 +152,7 @@ namespace Discord.Net.Queue if (millis <= 0 || !await _semaphore.WaitAsync(millis).ConfigureAwait(false)) throw new TimeoutException(); } - else - await _semaphore.WaitAsync().ConfigureAwait(false); + await _semaphore.WaitAsync().ConfigureAwait(false); } private async Task QueueExitAsync() { diff --git a/src/Discord.Net/Net/RateLimitException.cs b/src/Discord.Net/Net/RateLimitException.cs index 4fa4900e9..ff594155a 100644 --- a/src/Discord.Net/Net/RateLimitException.cs +++ b/src/Discord.Net/Net/RateLimitException.cs @@ -7,9 +7,10 @@ namespace Discord.Net public string BucketId { get; } public int RetryAfterMilliseconds { get; } - public HttpRateLimitException(int retryAfterMilliseconds) - : base((HttpStatusCode)429) + public HttpRateLimitException(string bucketId, int retryAfterMilliseconds, string reason) + : base((HttpStatusCode)429, reason) { + BucketId = bucketId; RetryAfterMilliseconds = retryAfterMilliseconds; } } diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index 43b405966..dd937d6ec 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -124,25 +124,32 @@ namespace Discord.Net.Rest int statusCode = (int)response.StatusCode; if (statusCode < 200 || statusCode >= 300) //2xx = Success { - if (statusCode == 429) - { - //TODO: Include bucket info - int retryAfterMillis = int.Parse(response.Headers.GetValues("retry-after").First()); - throw new HttpRateLimitException(retryAfterMillis); - } - string reason = null; - try + JToken content = null; + if (response.Content.Headers.GetValues("content-type").FirstOrDefault() == "application/json") { - using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var reader = new StreamReader(stream)) - using (var json = new JsonTextReader(reader)) + try { - reason = (_errorDeserializer.Deserialize(json) as JToken).Value("message"); + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = new StreamReader(stream)) + using (var json = new JsonTextReader(reader)) + { + content = _errorDeserializer.Deserialize(json); + reason = content.Value("message"); + } } + catch { } //Might have been HTML Should we check for content-type? + } + + if (statusCode == 429 && content != null) + { + //TODO: Include bucket info + string bucketId = content.Value("bucket"); + int retryAfterMillis = content.Value("retry_after"); + throw new HttpRateLimitException(bucketId, retryAfterMillis, reason); } - catch { } //Might have been HTML - throw new HttpException(response.StatusCode, reason); + else + throw new HttpException(response.StatusCode, reason); } if (headerOnly) From 04bf733774fd943961e31d258fb244f448c06725 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Jun 2016 21:51:56 -0300 Subject: [PATCH 103/160] Added a few missing ConfigureAwait(false) --- src/Discord.Net/DiscordSocketClient.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 33aa1a105..ac3a33116 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -167,7 +167,7 @@ namespace Discord await DisconnectInternalAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; - await _gatewayLogger.InfoAsync("Connecting"); + await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); try { _connectTask = new TaskCompletionSource(); @@ -178,7 +178,7 @@ namespace Discord await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; - await _gatewayLogger.InfoAsync("Connected"); + await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false); } catch (Exception) { @@ -203,7 +203,7 @@ namespace Discord if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - await _gatewayLogger.InfoAsync("Disconnecting"); + await _gatewayLogger.InfoAsync("Disconnecting").ConfigureAwait(false); //Signal tasks to complete try { _cancelToken.Cancel(); } catch { } @@ -475,7 +475,7 @@ namespace Discord case GatewayOpCode.InvalidSession: { await _gatewayLogger.DebugAsync("Received InvalidSession").ConfigureAwait(false); - await _gatewayLogger.WarningAsync("Failed to resume previous session"); + await _gatewayLogger.WarningAsync("Failed to resume previous session").ConfigureAwait(false); _sessionId = null; _lastSeq = 0; @@ -485,7 +485,7 @@ namespace Discord case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - await _gatewayLogger.WarningAsync("Server requested a reconnect"); + await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); await StartReconnectAsync().ConfigureAwait(false); } @@ -526,7 +526,7 @@ namespace Discord await Ready.RaiseAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete - await _gatewayLogger.InfoAsync("Ready"); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; @@ -586,7 +586,7 @@ namespace Discord } else { - await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild."); + await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild.").ConfigureAwait(false); return; } } From 5f18d3901142bd8936cdd0ac977f8880b178604d Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 02:28:43 -0300 Subject: [PATCH 104/160] Reworked event system, fixed presence and voice state updates --- src/Discord.Net/API/DiscordAPIClient.cs | 28 +-- src/Discord.Net/DiscordClient.cs | 17 +- src/Discord.Net/DiscordSocketClient.cs | 183 ++++++++++++------ src/Discord.Net/DiscordSocketConfig.cs | 2 - .../Entities/WebSocket/CachedGuild.cs | 24 +-- src/Discord.Net/Extensions/EventExtensions.cs | 56 ------ src/Discord.Net/Logging/LogManager.cs | 18 +- src/Discord.Net/Utilities/AsyncEvent.cs | 74 +++++++ 8 files changed, 242 insertions(+), 160 deletions(-) delete mode 100644 src/Discord.Net/Extensions/EventExtensions.cs create mode 100644 src/Discord.Net/Utilities/AsyncEvent.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 78396697a..e71dcccc6 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -1,6 +1,5 @@ using Discord.API.Gateway; using Discord.API.Rest; -using Discord.Extensions; using Discord.Net; using Discord.Net.Converters; using Discord.Net.Queue; @@ -24,10 +23,17 @@ namespace Discord.API { public class DiscordApiClient : IDisposable { - public event Func SentRequest; - public event Func SentGatewayMessage; - public event Func ReceivedGatewayEvent; - public event Func Disconnected; + private object _eventLock = new object(); + + public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } + private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + + public event Func ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } + private readonly AsyncEvent> _receivedGatewayEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); private readonly RequestQueue _requestQueue; private readonly JsonSerializer _serializer; @@ -67,19 +73,19 @@ namespace Discord.API using (var reader = new StreamReader(decompressed)) { var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); - await ReceivedGatewayEvent.RaiseAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); } } }; _gatewayClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); - await ReceivedGatewayEvent.RaiseAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); + await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); }; _gatewayClient.Closed += async ex => { await DisconnectAsync().ConfigureAwait(false); - await Disconnected.RaiseAsync(ex).ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); }; } @@ -311,7 +317,7 @@ namespace Discord.API stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - await SentRequest.RaiseAsync(method, endpoint, milliseconds).ConfigureAwait(false); + await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } @@ -324,7 +330,7 @@ namespace Discord.API stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - await SentRequest.RaiseAsync(method, endpoint, milliseconds).ConfigureAwait(false); + await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } @@ -344,7 +350,7 @@ namespace Discord.API if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); - await SentGatewayMessage.RaiseAsync((int)opCode).ConfigureAwait(false); + await _sentGatewayMessageEvent.InvokeAsync((int)opCode).ConfigureAwait(false); } //Auth diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index eafc06810..bc5601bb1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -15,8 +15,15 @@ namespace Discord { public class DiscordClient : IDiscordClient { - public event Func Log; - public event Func LoggedIn, LoggedOut; + private readonly object _eventLock = new object(); + + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + private readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + public event Func LoggedIn { add { _loggedInEvent.Add(value); } remove { _loggedInEvent.Remove(value); } } + private readonly AsyncEvent> _loggedInEvent = new AsyncEvent>(); + public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } + private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); internal readonly Logger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; @@ -35,7 +42,7 @@ namespace Discord public DiscordClient(DiscordConfig config) { _log = new LogManager(config.LogLevel); - _log.Message += async msg => await Log.RaiseAsync(msg).ConfigureAwait(false); + _log.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); _discordLogger = _log.CreateLogger("Discord"); _restLogger = _log.CreateLogger("Rest"); _queueLogger = _log.CreateLogger("Queue"); @@ -96,7 +103,7 @@ namespace Discord throw; } - await LoggedIn.RaiseAsync().ConfigureAwait(false); + await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } protected virtual Task OnLoginAsync() => Task.CompletedTask; @@ -123,7 +130,7 @@ namespace Discord LoginState = LoginState.LoggedOut; - await LoggedOut.RaiseAsync().ConfigureAwait(false); + await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } protected virtual Task OnLogoutAsync() => Task.CompletedTask; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index ac3a33116..b8f2f1afb 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -21,22 +21,74 @@ namespace Discord //TODO: Add resume logic public class DiscordSocketClient : DiscordClient, IDiscordClient { - public event Func Connected, Disconnected, Ready; - //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, GuildDownloadedMembers; - public event Func GuildUpdated; - public event Func UserJoined, UserLeft, UserBanned, UserUnbanned; - public event Func UserUpdated; - public event Func CurrentUserUpdated; - public event Func UserIsTyping; - public event Func LatencyUpdated; - //TODO: Add PresenceUpdated? VoiceStateUpdated? + private object _eventLock = new object(); + + //General + public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + + public event Func LatencyUpdated { add { _latencyUpdatedEvent.Add(value); } remove { _latencyUpdatedEvent.Remove(value); } } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + + //Channels + public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } remove { _channelCreatedEvent.Remove(value); } } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + public event Func ChannelDestroyed { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } + private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + public event Func ChannelUpdated { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } } + private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + public event Func MessageReceived { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } + private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); + public event Func, IMessage, Task> MessageUpdated { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } + private readonly AsyncEvent, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent, IMessage, Task>>(); + + //Roles + public event Func RoleCreated { add { _roleCreatedEvent.Add(value); } remove { _roleCreatedEvent.Remove(value); } } + private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + public event Func RoleDeleted { add { _roleDeletedEvent.Add(value); } remove { _roleDeletedEvent.Remove(value); } } + private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + public event Func RoleUpdated { add { _roleUpdatedEvent.Add(value); } remove { _roleUpdatedEvent.Remove(value); } } + private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + public event Func JoinedGuild { add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } + private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + public event Func LeftGuild { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } + private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + public event Func GuildAvailable { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } + private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + public event Func GuildUnavailable { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } + private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + public event Func GuildDownloadedMembers { add { _guildDownloadedMembersEvent.Add(value); } remove { _guildDownloadedMembersEvent.Remove(value); } } + private AsyncEvent> _guildDownloadedMembersEvent = new AsyncEvent>(); + public event Func GuildUpdated { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } + private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + public event Func UserJoined { add { _userJoinedEvent.Add(value); } remove { _userJoinedEvent.Remove(value); } } + private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + public event Func UserLeft { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } + private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + public event Func UserBanned { add { _userBannedEvent.Add(value); } remove { _userBannedEvent.Remove(value); } } + private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + public event Func UserUnbanned { add { _userUnbannedEvent.Add(value); } remove { _userUnbannedEvent.Remove(value); } } + private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + public event Func, IGuildUser, Task> UserUpdated { add { _userUpdatedEvent.Add(value); } remove { _userUpdatedEvent.Remove(value); } } + private readonly AsyncEvent, IGuildUser, Task>> _userUpdatedEvent = new AsyncEvent, IGuildUser, Task>>(); + public event Func CurrentUserUpdated { add { _selfUpdatedEvent.Add(value); } remove { _selfUpdatedEvent.Remove(value); } } + private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + public event Func UserIsTyping { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } + private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + + //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; @@ -46,7 +98,6 @@ namespace Discord private readonly DataStoreProvider _dataStoreProvider; private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; - private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; @@ -78,8 +129,7 @@ namespace Discord internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. - public DiscordSocketClient() - : this(new DiscordSocketConfig()) { } + public DiscordSocketClient() : this(new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket discord client. public DiscordSocketClient(DiscordSocketConfig config) : base(config) @@ -93,7 +143,6 @@ namespace Discord _dataStoreProvider = config.DataStoreProvider; MessageCacheSize = config.MessageCacheSize; - _enablePreUpdateEvents = config.EnablePreUpdateEvents; _largeThreshold = config.LargeThreshold; _gatewayLogger = _log.CreateLogger("Gateway"); @@ -173,7 +222,7 @@ namespace Discord _connectTask = new TaskCompletionSource(); _cancelToken = new CancellationTokenSource(); await ApiClient.ConnectAsync().ConfigureAwait(false); - await Connected.RaiseAsync().ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -228,7 +277,7 @@ namespace Discord ConnectionState = ConnectionState.Disconnected; await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); - await Disconnected.RaiseAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); } private async Task StartReconnectAsync() { @@ -463,12 +512,14 @@ namespace Discord var heartbeatTime = _heartbeatTime; if (heartbeatTime != 0) { - var latency = (int)(Environment.TickCount - _heartbeatTime); + int latency = (int)(Environment.TickCount - _heartbeatTime); _heartbeatTime = 0; await _gatewayLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + + int before = Latency; Latency = latency; - await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); } } break; @@ -523,7 +574,7 @@ namespace Discord _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); - await Ready.RaiseAsync().ConfigureAwait(false); + await _readyEvent.InvokeAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); @@ -546,7 +597,7 @@ namespace Discord if (data.Unavailable != false) { guild = AddGuild(data, DataStore); - await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); + await _joinedGuildEvent.InvokeAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Joined {data.Name}").ConfigureAwait(false); } else @@ -568,7 +619,7 @@ namespace Discord if (data.Unavailable != true) { await _gatewayLogger.VerboseAsync($"Connected to {data.Name}").ConfigureAwait(false); - await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); + await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); } } break; @@ -580,9 +631,9 @@ namespace Discord var guild = DataStore.GetGuild(data.Id); if (guild != null) { - var before = _enablePreUpdateEvents ? guild.Clone() : null; + var before = guild.Clone(); guild.Update(data, UpdateSource.WebSocket); - await GuildUpdated.RaiseAsync(before, guild).ConfigureAwait(false); + await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else { @@ -604,11 +655,11 @@ namespace Discord foreach (var member in guild.Members) member.User.RemoveRef(this); - await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); + await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); await _gatewayLogger.VerboseAsync($"Disconnected from {data.Name}").ConfigureAwait(false); if (data.Unavailable != true) { - await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); + await _leftGuildEvent.InvokeAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Left {data.Name}").ConfigureAwait(false); } else @@ -644,7 +695,7 @@ namespace Discord else channel = AddDMChannel(data, DataStore); if (channel != null) - await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); + await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); } break; case "CHANNEL_UPDATE": @@ -655,9 +706,9 @@ namespace Discord var channel = DataStore.GetChannel(data.Id); if (channel != null) { - var before = _enablePreUpdateEvents ? channel.Clone() : null; + var before = channel.Clone(); channel.Update(data, UpdateSource.WebSocket); - await ChannelUpdated.RaiseAsync(before, channel).ConfigureAwait(false); + await _channelUpdatedEvent.InvokeAsync(before, channel).ConfigureAwait(false); } else { @@ -686,7 +737,7 @@ namespace Discord else channel = RemoveDMChannel(data.Recipient.Value.Id); if (channel != null) - await ChannelDestroyed.RaiseAsync(channel).ConfigureAwait(false); + await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); @@ -705,7 +756,7 @@ namespace Discord if (guild != null) { var user = guild.AddUser(data, DataStore); - await UserJoined.RaiseAsync(user).ConfigureAwait(false); + await _userJoinedEvent.InvokeAsync(user).ConfigureAwait(false); } else { @@ -725,9 +776,9 @@ namespace Discord var user = guild.GetUser(data.User.Id); if (user != null) { - var before = _enablePreUpdateEvents ? user.Clone() : null; + var before = user.Clone(); user.Update(data, UpdateSource.WebSocket); - await UserUpdated.RaiseAsync(before, user).ConfigureAwait(false); + await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); } else { @@ -754,7 +805,7 @@ namespace Discord if (user != null) { user.User.RemoveRef(this); - await UserLeft.RaiseAsync(user).ConfigureAwait(false); + await _userLeftEvent.InvokeAsync(user).ConfigureAwait(false); } else { @@ -783,7 +834,7 @@ namespace Discord if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { guild.CompleteDownloadMembers(); - await GuildDownloadedMembers.RaiseAsync(guild).ConfigureAwait(false); + await _guildDownloadedMembersEvent.InvokeAsync(guild).ConfigureAwait(false); } } else @@ -804,7 +855,7 @@ namespace Discord if (guild != null) { var role = guild.AddRole(data.Role); - await RoleCreated.RaiseAsync(role).ConfigureAwait(false); + await _roleCreatedEvent.InvokeAsync(role).ConfigureAwait(false); } else { @@ -824,9 +875,9 @@ namespace Discord var role = guild.GetRole(data.Role.Id); if (role != null) { - var before = _enablePreUpdateEvents ? role.Clone() : null; + var before = role.Clone(); role.Update(data.Role, UpdateSource.WebSocket); - await RoleUpdated.RaiseAsync(before, role).ConfigureAwait(false); + await _roleUpdatedEvent.InvokeAsync(before, role).ConfigureAwait(false); } else { @@ -851,7 +902,7 @@ namespace Discord { var role = guild.RemoveRole(data.RoleId); if (role != null) - await RoleDeleted.RaiseAsync(role).ConfigureAwait(false); + await _roleDeletedEvent.InvokeAsync(role).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); @@ -874,7 +925,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserBanned.RaiseAsync(new User(data.User)).ConfigureAwait(false); + await _userBannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); @@ -889,7 +940,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await UserUnbanned.RaiseAsync(new User(data.User)).ConfigureAwait(false); + await _userUnbannedEvent.InvokeAsync(new User(data.User), guild).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); @@ -912,7 +963,7 @@ namespace Discord if (author != null) { var msg = channel.AddMessage(author, data); - await MessageReceived.RaiseAsync(msg).ConfigureAwait(false); + await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); } else { @@ -939,7 +990,7 @@ namespace Discord CachedMessage cachedMsg = channel.GetMessage(data.Id); if (cachedMsg != null) { - before = _enablePreUpdateEvents ? cachedMsg.Clone() : null; + before = cachedMsg.Clone(); cachedMsg.Update(data, UpdateSource.WebSocket); after = cachedMsg; } @@ -951,7 +1002,12 @@ namespace Discord after = new Message(channel, author, data); } if (after != null) - await MessageUpdated.RaiseAsync(before, after).ConfigureAwait(false); + await _messageUpdatedEvent.InvokeAsync(Optional.Create(before), after).ConfigureAwait(false); + else + { + await _gatewayLogger.WarningAsync("MESSAGE_UPDATE was unable to build an updated message.").ConfigureAwait(false); + return; + } } else { @@ -969,7 +1025,7 @@ namespace Discord if (channel != null) { var msg = channel.RemoveMessage(data.Id); - await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create(msg)).ConfigureAwait(false); } else { @@ -989,7 +1045,7 @@ namespace Discord foreach (var id in data.Ids) { var msg = channel.RemoveMessage(id); - await MessageDeleted.RaiseAsync(msg).ConfigureAwait(false); + await _messageDeletedEvent.InvokeAsync(msg.Id, Optional.Create(msg)).ConfigureAwait(false); } } else @@ -1014,7 +1070,20 @@ namespace Discord await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); break; } - guild.UpdatePresence(data, DataStore); + + var user = guild.GetUser(data.User.Id); + if (user != null) + { + var before = user.Clone(); + user.Update(data, UpdateSource.WebSocket); + await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); + } + else + { + user = guild.AddOrUpdateUser(data, DataStore); + user.Update(data, UpdateSource.WebSocket); + await _userUpdatedEvent.InvokeAsync(Optional.Create(), user).ConfigureAwait(false); + } } else { @@ -1034,7 +1103,7 @@ namespace Discord { var user = channel.GetUser(data.UserId, true); if (user != null) - await UserIsTyping.RaiseAsync(channel, user).ConfigureAwait(false); + await _userIsTypingEvent.InvokeAsync(user, channel).ConfigureAwait(false); } } break; @@ -1057,7 +1126,11 @@ namespace Discord var user = guild.GetUser(data.UserId); if (user != null) + { + var before = user.Clone(); user.Update(data, UpdateSource.WebSocket); + await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); + } } else { @@ -1076,9 +1149,9 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); if (data.Id == CurrentUser.Id) { - var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; + var before = CurrentUser.Clone(); CurrentUser.Update(data, UpdateSource.WebSocket); - await CurrentUserUpdated.RaiseAsync(before, CurrentUser).ConfigureAwait(false); + await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); } else { diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs index 04c3a0828..c1ddaa90d 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -24,8 +24,6 @@ namespace Discord /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage. /// public bool UsePermissionsCache { get; set; } = false;*/ - /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. - public bool EnablePreUpdateEvents { get; set; } = true; /// /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. /// Decreasing this may reduce CPU usage while increasing login time and network usage. diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 5bd9ae06b..a86557f02 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -90,11 +90,7 @@ namespace Discord _downloaderPromise.SetResult(true); for (int i = 0; i < model.Presences.Length; i++) - { - var presence = model.Presences[i]; - UpdatePresence(presence, dataStore, members); - //AddUser(presence, dataStore, members); - } + AddOrUpdateUser(model.Presences[i], dataStore, members); } _members = members; @@ -163,7 +159,7 @@ namespace Discord } return member; } - public CachedGuildUser AddUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) + public CachedGuildUser AddOrUpdateUser(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) { members = members ?? _members; @@ -194,22 +190,6 @@ namespace Discord return member; return null; } - public void UpdatePresence(PresenceModel model, DataStore dataStore, ConcurrentDictionary members = null) - { - members = members ?? _members; - - CachedGuildUser member; - if (members.TryGetValue(model.User.Id, out member)) - member.Update(model, UpdateSource.WebSocket); - else - { - var user = Discord.GetOrAddUser(model.User, dataStore); - member = new CachedGuildUser(this, user, model); - members[user.Id] = member; - user.AddRef(); - DownloadedMemberCount++; - } - } public async Task DownloadMembersAsync() { if (!HasAllMembers) diff --git a/src/Discord.Net/Extensions/EventExtensions.cs b/src/Discord.Net/Extensions/EventExtensions.cs deleted file mode 100644 index 4467af55c..000000000 --- a/src/Discord.Net/Extensions/EventExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Extensions -{ - internal static class EventExtensions - { - //TODO: Optimize these for if there is only 1 subscriber (can we do this?) - //TODO: Could we maintain our own list instead of generating one on every invocation? - public static async Task RaiseAsync(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 RaiseAsync(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 RaiseAsync(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 RaiseAsync(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); - } - } - public static async Task RaiseAsync(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - var subscriptions = eventHandler?.GetInvocationList(); - if (subscriptions != null) - { - for (int i = 0; i < subscriptions.Length; i++) - await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); - } - } - } -} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 2d1ef53c7..d428ae59f 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -1,5 +1,4 @@ -using Discord.Extensions; -using System; +using System; using System.Threading.Tasks; namespace Discord.Logging @@ -8,7 +7,8 @@ namespace Discord.Logging { public LogSeverity Level { get; } - public event Func Message; + public event Func Message { add { _messageEvent.Add(value); } remove { _messageEvent.Remove(value); } } + private readonly AsyncEvent> _messageEvent = new AsyncEvent>(); public LogManager(LogSeverity minSeverity) { @@ -18,32 +18,32 @@ namespace Discord.Logging public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); } public async Task LogAsync(LogSeverity severity, string source, Exception ex) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } async Task ILogger.LogAsync(LogSeverity severity, string message, Exception ex) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); } async Task ILogger.LogAsync(LogSeverity severity, FormattableString message, Exception ex) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); } async Task ILogger.LogAsync(LogSeverity severity, Exception ex) { if (severity <= Level) - await Message.RaiseAsync(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); + await _messageEvent.InvokeAsync(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); } public Task ErrorAsync(string source, string message, Exception ex = null) diff --git a/src/Discord.Net/Utilities/AsyncEvent.cs b/src/Discord.Net/Utilities/AsyncEvent.cs new file mode 100644 index 000000000..0a4d55ed7 --- /dev/null +++ b/src/Discord.Net/Utilities/AsyncEvent.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Discord +{ + internal class AsyncEvent + { + private readonly object _subLock = new object(); + internal ImmutableArray _subscriptions; + + public IReadOnlyList Subscriptions => _subscriptions; + + public AsyncEvent() + { + _subscriptions = ImmutableArray.Create(); + } + + public void Add(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Add(subscriber); + } + public void Remove(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Remove(subscriber); + } + } + + internal static class EventExtensions + { + public static async Task InvokeAsync(this AsyncEvent> eventHandler) + { + var subscribers = eventHandler.Subscriptions; + if (subscribers.Count > 0) + { + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); + } + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } +} From 43d3998c178fee9722f1de4d333ff9777aba453a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 03:09:42 -0300 Subject: [PATCH 105/160] Removed MESSAGE_UPDATE warning --- src/Discord.Net/DiscordSocketClient.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index b8f2f1afb..725927b1b 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1003,11 +1003,6 @@ namespace Discord } if (after != null) await _messageUpdatedEvent.InvokeAsync(Optional.Create(before), after).ConfigureAwait(false); - else - { - await _gatewayLogger.WarningAsync("MESSAGE_UPDATE was unable to build an updated message.").ConfigureAwait(false); - return; - } } else { From 45330a0032852b7ac3086b40d9272d40fe3c0a75 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 16:40:15 -0300 Subject: [PATCH 106/160] Dont generate property contracts for ignored properties --- src/Discord.Net/Net/Converters/DiscordContractResolver.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs index 89eea4f87..f30e38cdd 100644 --- a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -17,6 +17,9 @@ namespace Discord.Net.Converters protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); + if (property.Ignored) + return property; + var propInfo = member as PropertyInfo; if (propInfo != null) { From 31b0085ae34c06ca2734a58a45f9b83b6f32e99c Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 17:45:08 -0300 Subject: [PATCH 107/160] Reworked Presence and VoiceState, added updated events --- src/Discord.Net/DiscordSocketClient.Events.cs | 190 ++++++++++++++++++ src/Discord.Net/DiscordSocketClient.cs | 142 ++++--------- src/Discord.Net/Entities/Users/GuildUser.cs | 23 +-- src/Discord.Net/Entities/Users/IGuildUser.cs | 4 - src/Discord.Net/Entities/Users/IVoiceState.cs | 4 + .../Entities/WebSocket/CachedGuild.cs | 2 +- .../Entities/WebSocket/CachedGuildUser.cs | 26 ++- .../Entities/WebSocket/VoiceState.cs | 22 +- 8 files changed, 277 insertions(+), 136 deletions(-) create mode 100644 src/Discord.Net/DiscordSocketClient.Events.cs diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs new file mode 100644 index 000000000..2a16f7b65 --- /dev/null +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -0,0 +1,190 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + public partial class DiscordSocketClient + { + //General + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func Ready + { + add { _readyEvent.Add(value); } + remove { _readyEvent.Remove(value); } + } + private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + + //Channels + public event Func ChannelCreated + { + add { _channelCreatedEvent.Add(value); } + remove { _channelCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); + public event Func ChannelDestroyed + { + add { _channelDestroyedEvent.Add(value); } + remove { _channelDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); + public event Func ChannelUpdated + { + add { _channelUpdatedEvent.Add(value); } + remove { _channelUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); + + //Messages + public event Func MessageReceived + { + add { _messageReceivedEvent.Add(value); } + remove { _messageReceivedEvent.Remove(value); } + } + private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); + public event Func, Task> MessageDeleted + { + add { _messageDeletedEvent.Add(value); } + remove { _messageDeletedEvent.Remove(value); } + } + private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); + public event Func, IMessage, Task> MessageUpdated + { + add { _messageUpdatedEvent.Add(value); } + remove { _messageUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent, IMessage, Task>>(); + + //Roles + public event Func RoleCreated + { + add { _roleCreatedEvent.Add(value); } + remove { _roleCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); + public event Func RoleDeleted + { + add { _roleDeletedEvent.Add(value); } + remove { _roleDeletedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); + public event Func RoleUpdated + { + add { _roleUpdatedEvent.Add(value); } + remove { _roleUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); + + //Guilds + public event Func JoinedGuild + { + add { _joinedGuildEvent.Add(value); } + remove { _joinedGuildEvent.Remove(value); } + } + private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); + public event Func LeftGuild + { + add { _leftGuildEvent.Add(value); } + remove { _leftGuildEvent.Remove(value); } + } + private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); + public event Func GuildAvailable + { + add { _guildAvailableEvent.Add(value); } + remove { _guildAvailableEvent.Remove(value); } + } + private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); + public event Func GuildUnavailable + { + add { _guildUnavailableEvent.Add(value); } + remove { _guildUnavailableEvent.Remove(value); } + } + private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); + public event Func GuildDownloadedMembers + { + add { _guildDownloadedMembersEvent.Add(value); } + remove { _guildDownloadedMembersEvent.Remove(value); } + } + private AsyncEvent> _guildDownloadedMembersEvent = new AsyncEvent>(); + public event Func GuildUpdated + { + add { _guildUpdatedEvent.Add(value); } + remove { _guildUpdatedEvent.Remove(value); } + } + private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + + //Users + public event Func UserJoined + { + add { _userJoinedEvent.Add(value); } + remove { _userJoinedEvent.Remove(value); } + } + private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); + public event Func UserLeft + { + add { _userLeftEvent.Add(value); } + remove { _userLeftEvent.Remove(value); } + } + private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); + public event Func UserBanned + { + add { _userBannedEvent.Add(value); } + remove { _userBannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); + public event Func UserUnbanned + { + add { _userUnbannedEvent.Add(value); } + remove { _userUnbannedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); + public event Func UserUpdated + { + add { _userUpdatedEvent.Add(value); } + remove { _userUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); + public event Func UserPresenceUpdated + { + add { _userPresenceUpdatedEvent.Add(value); } + remove { _userPresenceUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userPresenceUpdatedEvent = new AsyncEvent>(); + public event Func UserVoiceStateUpdated + { + add { _userVoiceStateUpdatedEvent.Add(value); } + remove { _userVoiceStateUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _userVoiceStateUpdatedEvent = new AsyncEvent>(); + public event Func CurrentUserUpdated + { + add { _selfUpdatedEvent.Add(value); } + remove { _selfUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); + public event Func UserIsTyping + { + add { _userIsTypingEvent.Add(value); } + remove { _userIsTypingEvent.Remove(value); } + } + private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + + //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; + } +} diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 725927b1b..272ea5444 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -19,77 +19,8 @@ namespace Discord //TODO: Add event docstrings //TODO: Add reconnect logic (+ensure the heartbeat task to shut down) //TODO: Add resume logic - public class DiscordSocketClient : DiscordClient, IDiscordClient + public partial class DiscordSocketClient : DiscordClient, IDiscordClient { - private object _eventLock = new object(); - - //General - public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - public event Func Ready { add { _readyEvent.Add(value); } remove { _readyEvent.Remove(value); } } - private readonly AsyncEvent> _readyEvent = new AsyncEvent>(); - - public event Func LatencyUpdated { add { _latencyUpdatedEvent.Add(value); } remove { _latencyUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - - //Channels - public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } remove { _channelCreatedEvent.Remove(value); } } - private readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); - public event Func ChannelDestroyed { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } - private readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); - public event Func ChannelUpdated { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _channelUpdatedEvent = new AsyncEvent>(); - - //Messages - public event Func MessageReceived { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } - private readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); - public event Func, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - private readonly AsyncEvent, Task>> _messageDeletedEvent = new AsyncEvent, Task>>(); - public event Func, IMessage, Task> MessageUpdated { add { _messageUpdatedEvent.Add(value); } remove { _messageUpdatedEvent.Remove(value); } } - private readonly AsyncEvent, IMessage, Task>> _messageUpdatedEvent = new AsyncEvent, IMessage, Task>>(); - - //Roles - public event Func RoleCreated { add { _roleCreatedEvent.Add(value); } remove { _roleCreatedEvent.Remove(value); } } - private readonly AsyncEvent> _roleCreatedEvent = new AsyncEvent>(); - public event Func RoleDeleted { add { _roleDeletedEvent.Add(value); } remove { _roleDeletedEvent.Remove(value); } } - private readonly AsyncEvent> _roleDeletedEvent = new AsyncEvent>(); - public event Func RoleUpdated { add { _roleUpdatedEvent.Add(value); } remove { _roleUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _roleUpdatedEvent = new AsyncEvent>(); - - //Guilds - public event Func JoinedGuild { add { _joinedGuildEvent.Add(value); } remove { _joinedGuildEvent.Remove(value); } } - private AsyncEvent> _joinedGuildEvent = new AsyncEvent>(); - public event Func LeftGuild { add { _leftGuildEvent.Add(value); } remove { _leftGuildEvent.Remove(value); } } - private AsyncEvent> _leftGuildEvent = new AsyncEvent>(); - public event Func GuildAvailable { add { _guildAvailableEvent.Add(value); } remove { _guildAvailableEvent.Remove(value); } } - private AsyncEvent> _guildAvailableEvent = new AsyncEvent>(); - public event Func GuildUnavailable { add { _guildUnavailableEvent.Add(value); } remove { _guildUnavailableEvent.Remove(value); } } - private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildDownloadedMembers { add { _guildDownloadedMembersEvent.Add(value); } remove { _guildDownloadedMembersEvent.Remove(value); } } - private AsyncEvent> _guildDownloadedMembersEvent = new AsyncEvent>(); - public event Func GuildUpdated { add { _guildUpdatedEvent.Add(value); } remove { _guildUpdatedEvent.Remove(value); } } - private AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); - - //Users - public event Func UserJoined { add { _userJoinedEvent.Add(value); } remove { _userJoinedEvent.Remove(value); } } - private readonly AsyncEvent> _userJoinedEvent = new AsyncEvent>(); - public event Func UserLeft { add { _userLeftEvent.Add(value); } remove { _userLeftEvent.Remove(value); } } - private readonly AsyncEvent> _userLeftEvent = new AsyncEvent>(); - public event Func UserBanned { add { _userBannedEvent.Add(value); } remove { _userBannedEvent.Remove(value); } } - private readonly AsyncEvent> _userBannedEvent = new AsyncEvent>(); - public event Func UserUnbanned { add { _userUnbannedEvent.Add(value); } remove { _userUnbannedEvent.Remove(value); } } - private readonly AsyncEvent> _userUnbannedEvent = new AsyncEvent>(); - public event Func, IGuildUser, Task> UserUpdated { add { _userUpdatedEvent.Add(value); } remove { _userUpdatedEvent.Remove(value); } } - private readonly AsyncEvent, IGuildUser, Task>> _userUpdatedEvent = new AsyncEvent, IGuildUser, Task>>(); - public event Func CurrentUserUpdated { add { _selfUpdatedEvent.Add(value); } remove { _selfUpdatedEvent.Remove(value); } } - private readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); - public event Func UserIsTyping { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } - private readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); - - //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; - private readonly ConcurrentQueue _largeGuilds; private readonly Logger _gatewayLogger; #if BENCHMARK @@ -1066,19 +997,20 @@ namespace Discord break; } + IPresence before; var user = guild.GetUser(data.User.Id); if (user != null) { - var before = user.Clone(); + before = user.Presence.Clone(); user.Update(data, UpdateSource.WebSocket); - await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); } else { + before = new Presence(null, UserStatus.Offline); user = guild.AddOrUpdateUser(data, DataStore); - user.Update(data, UpdateSource.WebSocket); - await _userUpdatedEvent.InvokeAsync(Optional.Create(), user).ConfigureAwait(false); } + + await _userPresenceUpdatedEvent.InvokeAsync(user, before, user).ConfigureAwait(false); } else { @@ -1103,6 +1035,26 @@ namespace Discord } break; + //Users + case "USER_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (data.Id == CurrentUser.Id) + { + var before = CurrentUser.Clone(); + CurrentUser.Update(data, UpdateSource.WebSocket); + await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); + return; + } + } + break; + //Voice case "VOICE_STATE_UPDATE": { @@ -1114,17 +1066,27 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) { - if (data.ChannelId == null) - guild.RemoveVoiceState(data.UserId); + VoiceState before, after; + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? new VoiceState(null, null, false, false, false); + after = guild.AddOrUpdateVoiceState(data, DataStore); + } else - guild.AddOrUpdateVoiceState(data, DataStore); + { + before = guild.RemoveVoiceState(data.UserId) ?? new VoiceState(null, null, false, false, false); + after = new VoiceState(null, data); + } var user = guild.GetUser(data.UserId); if (user != null) { - var before = user.Clone(); - user.Update(data, UpdateSource.WebSocket); - await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); + await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); + return; } } else @@ -1136,26 +1098,6 @@ namespace Discord } break; - //Settings - case "USER_UPDATE": - { - await _gatewayLogger.DebugAsync("Received Dispatch (USER_UPDATE)").ConfigureAwait(false); - - var data = (payload as JToken).ToObject(_serializer); - if (data.Id == CurrentUser.Id) - { - var before = CurrentUser.Clone(); - CurrentUser.Update(data, UpdateSource.WebSocket); - await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); - } - else - { - await _gatewayLogger.WarningAsync("Received USER_UPDATE for wrong user.").ConfigureAwait(false); - return; - } - } - break; - //Ignored case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 0d4784417..7f21ee279 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -15,9 +15,7 @@ namespace Discord internal class GuildUser : IGuildUser, ISnowflakeEntity { private long? _joinedAtTicks; - - public bool IsDeaf { get; private set; } - public bool IsMute { get; private set; } + public string Nickname { get; private set; } public GuildPermissions GuildPermissions { get; private set; } @@ -59,11 +57,7 @@ namespace Discord public void Update(Model model, UpdateSource source) { if (source == UpdateSource.Rest && IsAttached) return; - - //if (model.Deaf.IsSpecified) - IsDeaf = model.Deaf; - //if (model.Mute.IsSpecified) - IsMute = model.Mute; + //if (model.JoinedAt.IsSpecified) _joinedAtTicks = model.JoinedAt.UtcTicks; if (model.Nick.IsSpecified) @@ -81,13 +75,6 @@ namespace Discord if (model.Nick.IsSpecified) Nickname = model.Nick.Value; } - public void Update(VoiceStateModel model, UpdateSource source) - { - if (source == UpdateSource.Rest && IsAttached) return; - - IsDeaf = model.Deaf; - IsMute = model.Mute; - } private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); @@ -127,10 +114,6 @@ namespace Discord if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.RoleIds.IsSpecified) { await Discord.ApiClient.ModifyGuildMemberAsync(Guild.Id, Id, args).ConfigureAwait(false); - if (args.Deaf.IsSpecified) - IsDeaf = args.Deaf.Value; - if (args.Mute.IsSpecified) - IsMute = args.Mute.Value; if (args.Nickname.IsSpecified) Nickname = args.Nickname.Value ?? ""; if (args.RoleIds.IsSpecified) @@ -161,6 +144,8 @@ namespace Discord IGuild IGuildUser.Guild => Guild; IReadOnlyCollection IGuildUser.Roles => Roles; + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; bool IVoiceState.IsSelfDeafened => false; bool IVoiceState.IsSelfMuted => false; bool IVoiceState.IsSuppressed => false; diff --git a/src/Discord.Net/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs index 280713d33..94728723e 100644 --- a/src/Discord.Net/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -8,10 +8,6 @@ namespace Discord /// A Guild-User pairing. public interface IGuildUser : IUpdateable, IUser, IVoiceState { - /// Returns true if the guild has deafened this user. - bool IsDeaf { get; } - /// Returns true if the guild has muted this user. - bool IsMute { get; } /// Gets when this user joined this guild. DateTimeOffset? JoinedAt { get; } /// Gets the nickname for this user. diff --git a/src/Discord.Net/Entities/Users/IVoiceState.cs b/src/Discord.Net/Entities/Users/IVoiceState.cs index 8bdd7436c..428601f2a 100644 --- a/src/Discord.Net/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net/Entities/Users/IVoiceState.cs @@ -2,6 +2,10 @@ { public interface IVoiceState { + /// Returns true if the guild has deafened this user. + bool IsDeafened { get; } + /// Returns true if the guild has muted this user. + bool IsMuted { get; } /// Returns true if this user has marked themselves as deafened. bool IsSelfDeafened { get; } /// Returns true if this user has marked themselves as muted. diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index a86557f02..2bd067da8 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -204,7 +204,7 @@ namespace Discord public VoiceState AddOrUpdateVoiceState(VoiceStateModel model, DataStore dataStore, ConcurrentDictionary voiceStates = null) { var voiceChannel = dataStore.GetChannel(model.ChannelId.Value) as CachedVoiceChannel; - var voiceState = new VoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress); + var voiceState = new VoiceState(voiceChannel, model); (voiceStates ?? _voiceStates)[model.UserId] = voiceState; return voiceState; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 294752d64..86036d188 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -3,17 +3,31 @@ using PresenceModel = Discord.API.Presence; namespace Discord { + //TODO: C#7 Candidate for record type + internal struct Presence : IPresence + { + public Game Game { get; } + public UserStatus Status { get; } + + public Presence(Game game, UserStatus status) + { + Game = game; + Status = status; + } + + public Presence Clone() => this; + } + internal class CachedGuildUser : GuildUser, ICachedUser { - private Game _game; - private UserStatus _status; + public Presence Presence { get; private set; } public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; public new CachedGlobalUser User => base.User as CachedGlobalUser; - public override Game Game => _game; - public override UserStatus Status => _status; + public override Game Game => Presence.Game; + public override UserStatus Status => Presence.Status; public VoiceState? VoiceState => Guild.GetVoiceState(Id); public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; @@ -34,8 +48,8 @@ namespace Discord { base.Update(model, source); - _status = model.Status; - _game = model.Game != null ? new Game(model.Game) : (Game)null; + var game = model.Game != null ? new Game(model.Game) : null; + Presence = new Presence(game, model.Status); } public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; diff --git a/src/Discord.Net/Entities/WebSocket/VoiceState.cs b/src/Discord.Net/Entities/WebSocket/VoiceState.cs index fc183a520..275108476 100644 --- a/src/Discord.Net/Entities/WebSocket/VoiceState.cs +++ b/src/Discord.Net/Entities/WebSocket/VoiceState.cs @@ -1,16 +1,20 @@ using System; +using Model = Discord.API.VoiceState; namespace Discord { + //TODO: C#7 Candidate for record type internal struct VoiceState : IVoiceState { [Flags] private enum Flags : byte { - None = 0x0, - Suppressed = 0x1, - SelfMuted = 0x2, - SelfDeafened = 0x4, + None = 0x00, + Suppressed = 0x01, + Muted = 0x02, + Deafened = 0x04, + SelfMuted = 0x08, + SelfDeafened = 0x10, } private readonly Flags _voiceStates; @@ -18,10 +22,14 @@ namespace Discord public CachedVoiceChannel VoiceChannel { get; } public string VoiceSessionId { get; } + public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; - public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; - + + public VoiceState(CachedVoiceChannel voiceChannel, Model model) + : this(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Suppress) { } public VoiceState(CachedVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isSuppressed) { VoiceChannel = voiceChannel; @@ -37,6 +45,8 @@ namespace Discord _voiceStates = voiceStates; } + public VoiceState Clone() => this; + IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } } From ea5da958b2e0564638d1a1027d9cea8a5fa71a8a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 18:06:52 -0300 Subject: [PATCH 108/160] Fixed CachedVoiceChannel.Members --- src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 1 + src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 2bd067da8..5dc26c1b5 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -42,6 +42,7 @@ namespace Discord } } public IReadOnlyCollection Members => _members.ToReadOnlyCollection(); + public IEnumerable> VoiceStates => _voiceStates; public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index a567ba2ad..6b00d82b5 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -12,7 +12,7 @@ namespace Discord public new CachedGuild Guild => base.Guild as CachedGuild; public IReadOnlyCollection Members - => Guild.Members.Where(x => x.VoiceChannel.Id == Id).ToImmutableArray(); + => Guild.VoiceStates.Where(x => x.Value.VoiceChannel.Id == Id).Select(x => Guild.GetUser(x.Key)).ToImmutableArray(); public CachedVoiceChannel(CachedGuild guild, Model model) : base(guild, model) From 38bfe52f2b5ca53df3de86a3cf018aad0dd9ece1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 18:10:09 -0300 Subject: [PATCH 109/160] Renamed GuildDownloadedMembers -> GuildMembersDownloaded --- src/Discord.Net/DiscordSocketClient.Events.cs | 8 ++++---- src/Discord.Net/DiscordSocketClient.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index 2a16f7b65..b4d4db47d 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -116,12 +116,12 @@ namespace Discord remove { _guildUnavailableEvent.Remove(value); } } private AsyncEvent> _guildUnavailableEvent = new AsyncEvent>(); - public event Func GuildDownloadedMembers + public event Func GuildMembersDownloaded { - add { _guildDownloadedMembersEvent.Add(value); } - remove { _guildDownloadedMembersEvent.Remove(value); } + add { _guildMembersDownloadedEvent.Add(value); } + remove { _guildMembersDownloadedEvent.Remove(value); } } - private AsyncEvent> _guildDownloadedMembersEvent = new AsyncEvent>(); + private AsyncEvent> _guildMembersDownloadedEvent = new AsyncEvent>(); public event Func GuildUpdated { add { _guildUpdatedEvent.Add(value); } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 272ea5444..545ebcf20 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -765,7 +765,7 @@ namespace Discord if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { guild.CompleteDownloadMembers(); - await _guildDownloadedMembersEvent.InvokeAsync(guild).ConfigureAwait(false); + await _guildMembersDownloadedEvent.InvokeAsync(guild).ConfigureAwait(false); } } else From 3276b2f03e82f4381e2ab62729c7373954a2a2b9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 21:54:45 -0300 Subject: [PATCH 110/160] Added VerificationLevel enum --- src/Discord.Net/API/Common/Guild.cs | 2 +- src/Discord.Net/Entities/Guilds/Guild.cs | 2 +- src/Discord.Net/Entities/Guilds/IGuild.cs | 3 ++- .../Entities/Guilds/VerificationLevel.cs | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/Discord.Net/Entities/Guilds/VerificationLevel.cs diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net/API/Common/Guild.cs index b4dfcdc11..e71a1b81b 100644 --- a/src/Discord.Net/API/Common/Guild.cs +++ b/src/Discord.Net/API/Common/Guild.cs @@ -25,7 +25,7 @@ namespace Discord.API [JsonProperty("embed_channel_id")] public ulong? EmbedChannelId { get; set; } [JsonProperty("verification_level")] - public int VerificationLevel { get; set; } + public VerificationLevel VerificationLevel { get; set; } [JsonProperty("voice_states")] public VoiceState[] VoiceStates { get; set; } [JsonProperty("roles")] diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index f17cb158c..ffe1faeb2 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -22,7 +22,7 @@ namespace Discord public string Name { get; private set; } public int AFKTimeout { get; private set; } public bool IsEmbeddable { get; private set; } - public int VerificationLevel { get; private set; } + public VerificationLevel VerificationLevel { get; private set; } public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 3300132b0..0d4d95284 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -13,7 +13,8 @@ namespace Discord int AFKTimeout { get; } /// Returns true if this guild is embeddable (e.g. widget) bool IsEmbeddable { get; } - int VerificationLevel { get; } + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + VerificationLevel VerificationLevel { get; } /// Returns the url to this guild's icon, or null if one is not set. string IconUrl { get; } /// Returns the url to this guild's splash image, or null if one is not set. diff --git a/src/Discord.Net/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net/Entities/Guilds/VerificationLevel.cs new file mode 100644 index 000000000..d6828b5c9 --- /dev/null +++ b/src/Discord.Net/Entities/Guilds/VerificationLevel.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public enum VerificationLevel + { + /// Users have no additional restrictions on sending messages to this guild. + None = 0, + /// Users must have a verified email on their account. + Low = 1, + /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. + Medium = 2, + /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. + High = 3 + } +} From d9802e9067212f2806779537413a204d9c8d7cd2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 22:34:30 -0300 Subject: [PATCH 111/160] Some prep work for shared datastores, removed datastoreprovider --- .../{DefaultDataStore.cs => DataStore.cs} | 38 +++++++++---------- src/Discord.Net/Data/DataStoreProvider.cs | 4 -- src/Discord.Net/Data/IDataStore.cs | 29 -------------- src/Discord.Net/Data/SharedDataStore.cs | 11 ------ src/Discord.Net/DiscordSocketClient.cs | 5 +-- src/Discord.Net/DiscordSocketConfig.cs | 9 +---- .../Entities/WebSocket/CachedGlobalUser.cs | 7 ++++ .../Entities/WebSocket/CachedGuild.cs | 3 +- 8 files changed, 30 insertions(+), 76 deletions(-) rename src/Discord.Net/Data/{DefaultDataStore.cs => DataStore.cs} (71%) delete mode 100644 src/Discord.Net/Data/DataStoreProvider.cs delete mode 100644 src/Discord.Net/Data/IDataStore.cs delete mode 100644 src/Discord.Net/Data/SharedDataStore.cs diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DataStore.cs similarity index 71% rename from src/Discord.Net/Data/DefaultDataStore.cs rename to src/Discord.Net/Data/DataStore.cs index b267f5932..59b62e180 100644 --- a/src/Discord.Net/Data/DefaultDataStore.cs +++ b/src/Discord.Net/Data/DataStore.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -namespace Discord.Data +namespace Discord { - public class DefaultDataStore : DataStore + public class DataStore { private const int CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2? private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 @@ -17,12 +17,12 @@ namespace Discord.Data private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; - internal override IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); - internal override IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); - internal override IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); - internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); + internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); + internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); + internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); - public DefaultDataStore(int guildCount, int dmChannelCount) + public DataStore(int guildCount, int dmChannelCount) { double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; double estimatedUsersCount = guildCount * AverageUsersPerGuild; @@ -32,18 +32,18 @@ namespace Discord.Data _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); } - internal override ICachedChannel GetChannel(ulong id) + internal ICachedChannel GetChannel(ulong id) { ICachedChannel channel; if (_channels.TryGetValue(id, out channel)) return channel; return null; } - internal override void AddChannel(ICachedChannel channel) + internal void AddChannel(ICachedChannel channel) { _channels[channel.Id] = channel; } - internal override ICachedChannel RemoveChannel(ulong id) + internal ICachedChannel RemoveChannel(ulong id) { ICachedChannel channel; if (_channels.TryRemove(id, out channel)) @@ -51,19 +51,19 @@ namespace Discord.Data return null; } - internal override CachedDMChannel GetDMChannel(ulong userId) + internal CachedDMChannel GetDMChannel(ulong userId) { CachedDMChannel channel; if (_dmChannels.TryGetValue(userId, out channel)) return channel; return null; } - internal override void AddDMChannel(CachedDMChannel channel) + internal void AddDMChannel(CachedDMChannel channel) { _channels[channel.Id] = channel; _dmChannels[channel.Recipient.Id] = channel; } - internal override CachedDMChannel RemoveDMChannel(ulong userId) + internal CachedDMChannel RemoveDMChannel(ulong userId) { CachedDMChannel channel; ICachedChannel ignored; @@ -75,18 +75,18 @@ namespace Discord.Data return null; } - internal override CachedGuild GetGuild(ulong id) + internal CachedGuild GetGuild(ulong id) { CachedGuild guild; if (_guilds.TryGetValue(id, out guild)) return guild; return null; } - internal override void AddGuild(CachedGuild guild) + internal void AddGuild(CachedGuild guild) { _guilds[guild.Id] = guild; } - internal override CachedGuild RemoveGuild(ulong id) + internal CachedGuild RemoveGuild(ulong id) { CachedGuild guild; if (_guilds.TryRemove(id, out guild)) @@ -94,18 +94,18 @@ namespace Discord.Data return null; } - internal override CachedGlobalUser GetUser(ulong id) + internal CachedGlobalUser GetUser(ulong id) { CachedGlobalUser user; if (_users.TryGetValue(id, out user)) return user; return null; } - internal override CachedGlobalUser GetOrAddUser(ulong id, Func userFactory) + internal CachedGlobalUser GetOrAddUser(ulong id, Func userFactory) { return _users.GetOrAdd(id, userFactory); } - internal override CachedGlobalUser RemoveUser(ulong id) + internal CachedGlobalUser RemoveUser(ulong id) { CachedGlobalUser user; if (_users.TryRemove(id, out user)) diff --git a/src/Discord.Net/Data/DataStoreProvider.cs b/src/Discord.Net/Data/DataStoreProvider.cs deleted file mode 100644 index db6ea3e56..000000000 --- a/src/Discord.Net/Data/DataStoreProvider.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.Data -{ - public delegate DataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount); -} diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs deleted file mode 100644 index 26d9c6e40..000000000 --- a/src/Discord.Net/Data/IDataStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord.Data -{ - public abstract class DataStore - { - internal abstract IReadOnlyCollection Channels { get; } - internal abstract IReadOnlyCollection DMChannels { get; } - internal abstract IReadOnlyCollection Guilds { get; } - internal abstract IReadOnlyCollection Users { get; } - - internal abstract ICachedChannel GetChannel(ulong id); - internal abstract void AddChannel(ICachedChannel channel); - internal abstract ICachedChannel RemoveChannel(ulong id); - - internal abstract CachedDMChannel GetDMChannel(ulong userId); - internal abstract void AddDMChannel(CachedDMChannel channel); - internal abstract CachedDMChannel RemoveDMChannel(ulong userId); - - internal abstract CachedGuild GetGuild(ulong id); - internal abstract void AddGuild(CachedGuild guild); - internal abstract CachedGuild RemoveGuild(ulong id); - - internal abstract CachedGlobalUser GetUser(ulong id); - internal abstract CachedGlobalUser GetOrAddUser(ulong userId, Func userFactory); - internal abstract CachedGlobalUser RemoveUser(ulong id); - } -} diff --git a/src/Discord.Net/Data/SharedDataStore.cs b/src/Discord.Net/Data/SharedDataStore.cs deleted file mode 100644 index fd3a6554a..000000000 --- a/src/Discord.Net/Data/SharedDataStore.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Discord.Data -{ - //TODO: Implement - //TODO: CachedPublicUser's GuildCount system is not at all multi-writer threadsafe! - //TODO: CachedPublicUser's Update method is not multi-writer threadsafe! - //TODO: Are there other multiwriters across shards? - - /*public class SharedDataStore - { - }*/ -} diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 545ebcf20..35891b1b6 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1,5 +1,4 @@ using Discord.API.Gateway; -using Discord.Data; using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; @@ -26,7 +25,6 @@ namespace Discord #if BENCHMARK private readonly Logger _benchmarkLogger; #endif - private readonly DataStoreProvider _dataStoreProvider; private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; private readonly int _largeThreshold; @@ -71,7 +69,6 @@ namespace Discord _connectionTimeout = config.ConnectionTimeout; _reconnectDelay = config.ReconnectDelay; _failedReconnectDelay = config.FailedReconnectDelay; - _dataStoreProvider = config.DataStoreProvider; MessageCacheSize = config.MessageCacheSize; _largeThreshold = config.LargeThreshold; @@ -481,7 +478,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); + var dataStore = new DataStore( data.Guilds.Length, data.PrivateChannels.Length); var currentUser = new CachedSelfUser(this, data.User); int unavailableGuilds = 0; diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs index c1ddaa90d..a40fba0e7 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -1,5 +1,4 @@ -using Discord.Data; -using Discord.Net.WebSockets; +using Discord.Net.WebSockets; namespace Discord { @@ -29,11 +28,7 @@ namespace Discord /// Decreasing this may reduce CPU usage while increasing login time and network usage. /// 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; } = () => new DefaultWebSocketClient(); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs index e07472ae8..6d101870e 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs @@ -1,4 +1,5 @@ using System; +using Discord.API; using Model = Discord.API.User; namespace Discord @@ -33,6 +34,12 @@ namespace Discord } } + public override void Update(Model model, UpdateSource source) + { + lock (this) + base.Update(model, source); + } + public CachedGlobalUser Clone() => MemberwiseClone() as CachedGlobalUser; ICachedUser ICachedUser.Clone() => Clone(); } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 5dc26c1b5..4d0af0783 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -1,5 +1,4 @@ -using Discord.Data; -using Discord.Extensions; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; From 11c9b61495c556226f960326ba5cff4037a0dad8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 24 Jun 2016 23:37:40 -0300 Subject: [PATCH 112/160] Added MfaLevel --- src/Discord.Net/API/Common/Guild.cs | 2 ++ src/Discord.Net/Entities/Guilds/Guild.cs | 6 ++++-- src/Discord.Net/Entities/Guilds/MfaLevel.cs | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net/Entities/Guilds/MfaLevel.cs diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net/API/Common/Guild.cs index e71a1b81b..2debcf408 100644 --- a/src/Discord.Net/API/Common/Guild.cs +++ b/src/Discord.Net/API/Common/Guild.cs @@ -34,5 +34,7 @@ namespace Discord.API public Emoji[] Emojis { get; set; } [JsonProperty("features")] public string[] Features { get; set; } + [JsonProperty("mfa_level")] + public MfaLevel MfaLevel { get; set; } } } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index ffe1faeb2..2ab46c617 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -23,12 +23,13 @@ namespace Discord public int AFKTimeout { get; private set; } public bool IsEmbeddable { get; private set; } public VerificationLevel VerificationLevel { get; private set; } + public MfaLevel MfaLevel { get; private set; } + public override DiscordClient Discord { get; } public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } public ulong OwnerId { get; private set; } public string VoiceRegionId { get; private set; } - public override DiscordClient Discord { get; } public ImmutableArray Emojis { get; protected set; } public ImmutableArray Features { get; protected set; } @@ -60,7 +61,8 @@ namespace Discord VoiceRegionId = model.Region; _splashId = model.Splash; VerificationLevel = model.VerificationLevel; - + MfaLevel = model.MfaLevel; + if (model.Emojis != null) { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); diff --git a/src/Discord.Net/Entities/Guilds/MfaLevel.cs b/src/Discord.Net/Entities/Guilds/MfaLevel.cs new file mode 100644 index 000000000..1dfef17d5 --- /dev/null +++ b/src/Discord.Net/Entities/Guilds/MfaLevel.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum MfaLevel + { + /// Users have no additional MFA restriction on this guild. + Disabled = 0, + /// Users must have MFA enabled on their account to perform administrative actions. + Enabled = 1 + } +} From 26eb572fdb500039bdf2bf0e4afd2215f275b4f5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Jun 2016 00:14:20 -0300 Subject: [PATCH 113/160] Added DefaultMessageNotifications --- src/Discord.Net/API/Common/Guild.cs | 2 ++ src/Discord.Net/API/DiscordAPIClient.cs | 1 - .../Entities/Guilds/DefaultMessageNotifications.cs | 10 ++++++++++ src/Discord.Net/Entities/Guilds/Guild.cs | 2 ++ src/Discord.Net/Entities/Guilds/IGuild.cs | 4 ++++ 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net/API/Common/Guild.cs index 2debcf408..823405ea7 100644 --- a/src/Discord.Net/API/Common/Guild.cs +++ b/src/Discord.Net/API/Common/Guild.cs @@ -36,5 +36,7 @@ namespace Discord.API public string[] Features { get; set; } [JsonProperty("mfa_level")] public MfaLevel MfaLevel { get; set; } + [JsonProperty("default_message_notifications")] + public DefaultMessageNotifications DefaultMessageNotifications { get; set; } } } diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index e71dcccc6..41e59e12f 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -543,7 +543,6 @@ namespace Discord.API Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); Preconditions.GreaterThan(args.OwnerId, 0, nameof(args.OwnerId)); Preconditions.NotNull(args.Region, nameof(args.Region)); - Preconditions.AtLeast(args.VerificationLevel, 0, nameof(args.VerificationLevel)); return await SendAsync("PATCH", $"guilds/{guildId}", args, options: options).ConfigureAwait(false); } diff --git a/src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs b/src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs new file mode 100644 index 000000000..efc107537 --- /dev/null +++ b/src/Discord.Net/Entities/Guilds/DefaultMessageNotifications.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum DefaultMessageNotifications + { + /// By default, only mentions will trigger notifications. + MentionsOnly = 0, + /// By default, all messages will trigger notifications. + AllMessages = 1 + } +} diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 2ab46c617..d4a47f302 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -24,6 +24,7 @@ namespace Discord public bool IsEmbeddable { get; private set; } public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } + public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } public override DiscordClient Discord { get; } public ulong? AFKChannelId { get; private set; } @@ -62,6 +63,7 @@ namespace Discord _splashId = model.Splash; VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; + DefaultMessageNotifications = model.DefaultMessageNotifications; if (model.Emojis != null) { diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 0d4d95284..7302e15f8 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -13,6 +13,10 @@ namespace Discord int AFKTimeout { get; } /// Returns true if this guild is embeddable (e.g. widget) bool IsEmbeddable { get; } + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + DefaultMessageNotifications DefaultMessageNotifications { get; } + /// Gets the level of mfa requirements a user must fulfill before being allowed to perform administrative actions in this guild. + MfaLevel MfaLevel { get; } /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. VerificationLevel VerificationLevel { get; } /// Returns the url to this guild's icon, or null if one is not set. From e6879cbe0f970fb072c80789bc729570c6d9d274 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Jun 2016 00:23:12 -0300 Subject: [PATCH 114/160] Added VerificationLevel and DefaultMessageNotifications to Guild.ModifyAsync --- src/Discord.Net/API/Rest/ModifyGuildParams.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/API/Rest/ModifyGuildParams.cs b/src/Discord.Net/API/Rest/ModifyGuildParams.cs index d610ab37e..bbdd46d28 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildParams.cs @@ -10,7 +10,9 @@ namespace Discord.API.Rest [JsonProperty("region")] public Optional Region { get; set; } [JsonProperty("verification_level")] - public Optional VerificationLevel { get; set; } + public Optional VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public Optional DefaultMessageNotifications { get; set; } [JsonProperty("afk_timeout")] public Optional AFKTimeout { get; set; } [JsonProperty("icon"), Image] From febd9636f7041507abad4e9182c7ac3cc3ea5114 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Jun 2016 00:33:47 -0300 Subject: [PATCH 115/160] Added ISelfUser.IsMfaEnabled --- src/Discord.Net/API/Common/User.cs | 10 +++++++--- src/Discord.Net/Entities/Users/ISelfUser.cs | 2 ++ src/Discord.Net/Entities/Users/SelfUser.cs | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net/API/Common/User.cs index 7e37d2cf8..9c4ff6911 100644 --- a/src/Discord.Net/API/Common/User.cs +++ b/src/Discord.Net/API/Common/User.cs @@ -10,13 +10,17 @@ namespace Discord.API public string Username { get; set; } [JsonProperty("discriminator")] public string Discriminator { get; set; } + [JsonProperty("bot")] + public bool Bot { get; set; } [JsonProperty("avatar")] public string Avatar { get; set; } + + //CurrentUser [JsonProperty("verified")] - public bool IsVerified { get; set; } + public bool Verified { get; set; } [JsonProperty("email")] public string Email { get; set; } - [JsonProperty("bot")] - public bool Bot { get; set; } + [JsonProperty("mfa_enabled")] + public bool MfaEnabled { get; set; } } } diff --git a/src/Discord.Net/Entities/Users/ISelfUser.cs b/src/Discord.Net/Entities/Users/ISelfUser.cs index 8efd0cef4..40f43b019 100644 --- a/src/Discord.Net/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net/Entities/Users/ISelfUser.cs @@ -10,6 +10,8 @@ namespace Discord string Email { get; } /// Returns true if this user's email has been verified. bool IsVerified { get; } + /// Returns true if this user has enabled MFA on their account. + bool IsMfaEnabled { get; } Task ModifyAsync(Action func); } diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 27ea3f178..393a1ad3a 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -9,6 +9,7 @@ namespace Discord { public string Email { get; private set; } public bool IsVerified { get; private set; } + public bool IsMfaEnabled { get; private set; } public override DiscordClient Discord { get; } @@ -24,7 +25,8 @@ namespace Discord base.Update(model, source); Email = model.Email; - IsVerified = model.IsVerified; + IsVerified = model.Verified; + IsMfaEnabled = model.MfaEnabled; } public async Task UpdateAsync() From 250a7deba7ed490975b049c2e565f6cde93aa949 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Jun 2016 00:55:39 -0300 Subject: [PATCH 116/160] Added Message pinning --- src/Discord.Net/API/Common/Message.cs | 4 +++- src/Discord.Net/API/DiscordAPIClient.cs | 22 +++++++++++++++++++ src/Discord.Net/Entities/Messages/IMessage.cs | 6 +++++ src/Discord.Net/Entities/Messages/Message.cs | 22 ++++++++++++++++--- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs index 3e6d69af9..be5305114 100644 --- a/src/Discord.Net/API/Common/Message.cs +++ b/src/Discord.Net/API/Common/Message.cs @@ -20,12 +20,14 @@ namespace Discord.API [JsonProperty("tts")] public Optional IsTextToSpeech { get; set; } [JsonProperty("mention_everyone")] - public Optional IsMentioningEveryone { get; set; } + public Optional MentionEveryone { get; set; } [JsonProperty("mentions")] public Optional Mentions { get; set; } [JsonProperty("attachments")] public Optional Attachments { get; set; } [JsonProperty("embeds")] public Optional Embeds { get; set; } + [JsonProperty("pinned")] + public Optional Pinned { get; set; } } } diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 41e59e12f..c138e455f 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -494,15 +494,37 @@ namespace Discord.API //Channel Permissions public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); Preconditions.NotNull(args, nameof(args)); await SendAsync("PUT", $"channels/{channelId}/permissions/{targetId}", args, options: options).ConfigureAwait(false); } public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null) { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(targetId, 0, nameof(targetId)); + await SendAsync("DELETE", $"channels/{channelId}/permissions/{targetId}", options: options).ConfigureAwait(false); } + //Channel Pins + public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.GreaterThan(channelId, 0, nameof(channelId)); + Preconditions.GreaterThan(messageId, 0, nameof(messageId)); + + await SendAsync("PUT", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false); + + } + public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + await SendAsync("DELETE", $"channels/{channelId}/pins/{messageId}", options: options).ConfigureAwait(false); + } + //Guilds public async Task GetGuildAsync(ulong guildId, RequestOptions options = null) { diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 0faf0837e..3a91336d2 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -11,6 +11,8 @@ namespace Discord DateTimeOffset? EditedTimestamp { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } + /// Returns true if this message was added to its channel's pinned messages. + bool IsPinned { get; } /// Returns the original, unprocessed text for this message. string RawText { get; } /// Returns the text for this message after mention processing. @@ -35,5 +37,9 @@ namespace Discord /// Modifies this message. Task ModifyAsync(Action func); + /// Adds this message to its channel's pinned messages. + Task PinAsync(); + /// Removes this message from its channel's pinned messages. + Task UnpinAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index f737ede44..bd91de78a 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -18,6 +18,7 @@ namespace Discord public bool IsTTS { get; private set; } public string RawText { get; private set; } public string Text { get; private set; } + public bool IsPinned { get; private set; } public IMessageChannel Channel { get; } public IUser Author { get; } @@ -57,13 +58,15 @@ namespace Discord if (model.IsTextToSpeech.IsSpecified) IsTTS = model.IsTextToSpeech.Value; + if (model.Pinned.IsSpecified) + IsPinned = model.Pinned.Value; if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; if (model.EditedTimestamp.IsSpecified) _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; - if (model.IsMentioningEveryone.IsSpecified) - _isMentioningEveryone = model.IsMentioningEveryone.Value; - + if (model.MentionEveryone.IsSpecified) + _isMentioningEveryone = model.MentionEveryone.Value; + if (model.Attachments.IsSpecified) { var value = model.Attachments.Value; @@ -144,6 +147,9 @@ namespace Discord model = await Discord.ApiClient.ModifyMessageAsync(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else model = await Discord.ApiClient.ModifyDMMessageAsync(Channel.Id, Id, args).ConfigureAwait(false); + { + await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); + } Update(model, UpdateSource.Rest); } public async Task DeleteAsync() @@ -154,6 +160,16 @@ namespace Discord else await Discord.ApiClient.DeleteDMMessageAsync(Channel.Id, Id).ConfigureAwait(false); } + /// Adds this message to its channel's pinned messages. + public async Task PinAsync() + { + await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); + } + /// Removes this message from its channel's pinned messages. + public async Task UnpinAsync() + { + await Discord.ApiClient.RemovePinAsync(Channel.Id, Id).ConfigureAwait(false); + } public override string ToString() => Text; private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Length > 0 ? $" [{Attachments.Length} Attachments]" : "")}"; From 2c87e496e19c1af774e605851b91efe78c133178 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 01:55:33 -0300 Subject: [PATCH 117/160] Added IUser.DiscriminatorValue --- src/Discord.Net/Entities/Users/GuildUser.cs | 1 + src/Discord.Net/Entities/Users/IUser.cs | 4 ++-- src/Discord.Net/Entities/Users/User.cs | 6 +++--- src/Discord.Net/Entities/WebSocket/CachedDMUser.cs | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 7f21ee279..31ea9f01c 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -27,6 +27,7 @@ namespace Discord public string AvatarUrl => User.AvatarUrl; public DateTimeOffset CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; + public ushort DiscriminatorValue => User.DiscriminatorValue; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index 44297f12e..5eef8231c 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - namespace Discord { public interface IUser : ISnowflakeEntity, IMentionable, IPresence @@ -8,6 +6,8 @@ namespace Discord string AvatarUrl { get; } /// Gets the per-username unique id for this user. string Discriminator { get; } + /// Gets the per-username unique id for this user. + ushort DiscriminatorValue { get; } /// Returns true if this user is a bot account. bool IsBot { get; } /// Gets the username for this user. diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index fdfc06abf..490128e82 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -8,15 +8,15 @@ namespace Discord internal class User : SnowflakeEntity, IUser { private string _avatarId; - private ushort _discriminator; public bool IsBot { get; private set; } public string Username { get; private set; } + public ushort DiscriminatorValue { get; private set; } public override DiscordClient Discord { get { throw new NotSupportedException(); } } public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); - public string Discriminator => _discriminator.ToString("D4"); + public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.Mention(this, false); public string NicknameMention => MentionUtils.Mention(this, true); public virtual Game Game => null; @@ -32,7 +32,7 @@ namespace Discord if (source == UpdateSource.Rest && IsAttached) return; _avatarId = model.Avatar; - _discriminator = ushort.Parse(model.Discriminator); + DiscriminatorValue = ushort.Parse(model.Discriminator); IsBot = model.Bot; Username = model.Username; } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs index de69c7c91..78d78933f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs @@ -16,6 +16,7 @@ namespace Discord public string AvatarUrl => User.AvatarUrl; public DateTimeOffset CreatedAt => User.CreatedAt; public string Discriminator => User.Discriminator; + public ushort DiscriminatorValue => User.DiscriminatorValue; public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; From d934a5a1ebafcaa30c2936b7000766cc5d1f22dc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 01:55:51 -0300 Subject: [PATCH 118/160] Added MentionUtils.TryParseXXX methods --- src/Discord.Net/Utilities/MentionUtils.cs | 55 ++++++++++++++++------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net/Utilities/MentionUtils.cs b/src/Discord.Net/Utilities/MentionUtils.cs index 0053792c2..1a092fd9c 100644 --- a/src/Discord.Net/Utilities/MentionUtils.cs +++ b/src/Discord.Net/Utilities/MentionUtils.cs @@ -19,6 +19,14 @@ namespace Discord /// Parses a provided user mention string. public static ulong ParseUser(string mentionText) + { + ulong id; + if (TryParseUser(mentionText, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(mentionText)); + } + /// Tries to parse a provided user mention string. + public static bool TryParseUser(string mentionText, out ulong userId) { mentionText = mentionText.Trim(); if (mentionText.Length >= 3 && mentionText[0] == '<' && mentionText[1] == '@' && mentionText[mentionText.Length - 1] == '>') @@ -27,40 +35,57 @@ namespace Discord mentionText = mentionText.Substring(3, mentionText.Length - 4); //<@!123> else mentionText = mentionText.Substring(2, mentionText.Length - 3); //<@123> - - ulong id; - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - return id; + + if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out userId)) + return true; } - throw new ArgumentException("Invalid mention format", nameof(mentionText)); + userId = 0; + return false; } + /// Parses a provided channel mention string. public static ulong ParseChannel(string mentionText) + { + ulong id; + if (TryParseChannel(mentionText, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(mentionText)); + } + /// Tries to parse a provided channel mention string. + public static bool TryParseChannel(string mentionText, out ulong channelId) { mentionText = mentionText.Trim(); if (mentionText.Length >= 3 && mentionText[0] == '<' && mentionText[1] == '#' && mentionText[mentionText.Length - 1] == '>') { mentionText = mentionText.Substring(2, mentionText.Length - 3); //<#123> - - ulong id; - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - return id; + + if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out channelId)) + return true; } - throw new ArgumentException("Invalid mention format", nameof(mentionText)); + channelId = 0; + return false; } /// Parses a provided role mention string. public static ulong ParseRole(string mentionText) + { + ulong id; + if (TryParseRole(mentionText, out id)) + return id; + throw new ArgumentException("Invalid mention format", nameof(mentionText)); + } + /// Tries to parse a provided role mention string. + public static bool TryParseRole(string mentionText, out ulong roleId) { mentionText = mentionText.Trim(); if (mentionText.Length >= 4 && mentionText[0] == '<' && mentionText[1] == '@' && mentionText[2] == '&' && mentionText[mentionText.Length - 1] == '>') { mentionText = mentionText.Substring(3, mentionText.Length - 4); //<@&123> - - ulong id; - if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out id)) - return id; + + if (ulong.TryParse(mentionText, NumberStyles.None, CultureInfo.InvariantCulture, out roleId)) + return true; } - throw new ArgumentException("Invalid mention format", nameof(mentionText)); + roleId = 0; + return false; } /// Gets the ids of all users mentioned in a provided text. From f59b6b900499242ce43ab795d37a84e974f28049 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 03:35:40 -0300 Subject: [PATCH 119/160] Implemented command type readers, parser and service. --- .../Attributes/CommandAttribute.cs | 11 +- src/Discord.Net.Commands/Command.cs | 104 +++++++++++-- src/Discord.Net.Commands/CommandError.cs | 20 +++ src/Discord.Net.Commands/CommandParameter.cs | 34 +++++ src/Discord.Net.Commands/CommandParser.cs | 144 ++++++++++++++++++ src/Discord.Net.Commands/CommandService.cs | 129 +++++++++++++++- src/Discord.Net.Commands/Module.cs | 17 ++- .../Readers/ChannelTypeReader.cs | 48 ++++++ .../Readers/GenericTypeReader.cs | 17 +++ .../Readers/MessageTypeReader.cs | 24 +++ .../Readers/RoleTypeReader.cs | 36 +++++ .../Readers/TypeReader.cs | 9 ++ .../Readers/UserTypeReader.cs | 66 ++++++++ .../Results/ExecuteResult.cs | 35 +++++ src/Discord.Net.Commands/Results/IResult.cs | 9 ++ .../Results/ParseResult.cs | 35 +++++ .../Results/SearchResult.cs | 33 ++++ .../Results/TypeReaderResult.cs | 30 ++++ src/Discord.Net.Commands/SearchResults.cs | 16 -- 19 files changed, 775 insertions(+), 42 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandError.cs create mode 100644 src/Discord.Net.Commands/CommandParameter.cs create mode 100644 src/Discord.Net.Commands/CommandParser.cs create mode 100644 src/Discord.Net.Commands/Readers/ChannelTypeReader.cs create mode 100644 src/Discord.Net.Commands/Readers/GenericTypeReader.cs create mode 100644 src/Discord.Net.Commands/Readers/MessageTypeReader.cs create mode 100644 src/Discord.Net.Commands/Readers/RoleTypeReader.cs create mode 100644 src/Discord.Net.Commands/Readers/TypeReader.cs create mode 100644 src/Discord.Net.Commands/Readers/UserTypeReader.cs create mode 100644 src/Discord.Net.Commands/Results/ExecuteResult.cs create mode 100644 src/Discord.Net.Commands/Results/IResult.cs create mode 100644 src/Discord.Net.Commands/Results/ParseResult.cs create mode 100644 src/Discord.Net.Commands/Results/SearchResult.cs create mode 100644 src/Discord.Net.Commands/Results/TypeReaderResult.cs delete mode 100644 src/Discord.Net.Commands/SearchResults.cs diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index db4e877d8..014668405 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -6,13 +6,14 @@ namespace Discord.Commands public class CommandAttribute : Attribute { public string Text { get; } - public string Name { get; } - public CommandAttribute(string name) : this(name, name) { } - public CommandAttribute(string text, string name) + public CommandAttribute() { - Text = text.ToLowerInvariant(); - Name = name; + Text = null; + } + public CommandAttribute(string text) + { + Text = text; } } } diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 568b645d9..d3b94b94c 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -1,35 +1,121 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.Reflection; +using System.Threading.Tasks; namespace Discord.Commands { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Command { - private Action _action; + private readonly object _instance; + private readonly Func, Task> _action; public string Name { get; } public string Description { get; } public string Text { get; } + public Module Module { get; } + public IReadOnlyList Parameters { get; } - internal Command(CommandAttribute attribute, MethodInfo methodInfo) + internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo) { + Module = module; + _instance = instance; + + Name = methodInfo.Name; + Text = attribute.Text; + var description = methodInfo.GetCustomAttribute(); if (description != null) Description = description.Text; - Name = attribute.Name; - Text = attribute.Text; + Parameters = BuildParameters(methodInfo); + _action = BuildAction(methodInfo); } - public void Invoke(IMessage msg) + public async Task Parse(IMessage msg, SearchResult searchResult) { - _action.Invoke(msg); + if (!searchResult.IsSuccess) + return ParseResult.FromError(searchResult); + + return await CommandParser.ParseArgs(this, msg, searchResult.ArgText, 0).ConfigureAwait(false); + } + public async Task Execute(IMessage msg, ParseResult parseResult) + { + if (!parseResult.IsSuccess) + return ExecuteResult.FromError(parseResult); + + try + { + await _action.Invoke(msg, parseResult.Values);//Note: This code may need context + return ExecuteResult.FromSuccess(); + } + catch (Exception ex) + { + return ExecuteResult.FromError(ex); + } } - private void BuildAction() + private IReadOnlyList BuildParameters(MethodInfo methodInfo) { - _action = null; - //TODO: Implement + var parameters = methodInfo.GetParameters(); + var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length - 1); + for (int i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var type = parameter.ParameterType; + + if (i == 0) + { + if (type != typeof(IMessage)) + throw new InvalidOperationException("The first parameter of a command must be IMessage."); + else + continue; + } + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsEnum) + type = Enum.GetUnderlyingType(type); + + var reader = Module.Service.GetTypeReader(type); + if (reader == null) + throw new InvalidOperationException($"This type ({type.FullName}) is not supported."); + + bool isUnparsed = parameter.GetCustomAttribute() != null; + if (isUnparsed) + { + if (type != typeof(string)) + throw new InvalidOperationException("Unparsed parameters only support the string type."); + else if (i != parameters.Length - 1) + throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command."); + } + + string name = parameter.Name; + string description = typeInfo.GetCustomAttribute()?.Text; + bool isOptional = parameter.IsOptional; + object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; + + paramBuilder.Add(new CommandParameter(name, description, reader, isOptional, isUnparsed, defaultValue)); + } + return paramBuilder.ToImmutable(); + } + private Func, Task> BuildAction(MethodInfo methodInfo) + { + //TODO: Temporary reflection hack. Lets build an actual expression tree here. + return (msg, args) => + { + object[] newArgs = new object[args.Count + 1]; + newArgs[0] = msg; + for (int i = 0; i < args.Count; i++) + newArgs[i + 1] = args[i]; + var result = methodInfo.Invoke(_instance, newArgs); + return result as Task ?? Task.CompletedTask; + }; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; } } diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs new file mode 100644 index 000000000..135930dd9 --- /dev/null +++ b/src/Discord.Net.Commands/CommandError.cs @@ -0,0 +1,20 @@ +namespace Discord.Commands +{ + public enum CommandError + { + //Search + UnknownCommand, + + //Parse + ParseFailed, + BadArgCount, + + //Parse (Type Reader) + CastFailed, + ObjectNotFound, + MultipleMatches, + + //Execute + Exception, + } +} diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs new file mode 100644 index 000000000..0a5952c48 --- /dev/null +++ b/src/Discord.Net.Commands/CommandParameter.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + //TODO: Add support for Multiple + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CommandParameter + { + private readonly TypeReader _reader; + + public string Name { get; } + public string Description { get; } + public bool IsOptional { get; } + public bool IsUnparsed { get; } + internal object DefaultValue { get; } + + public CommandParameter(string name, string description, TypeReader reader, bool isOptional, bool isUnparsed, object defaultValue) + { + _reader = reader; + IsOptional = isOptional; + IsUnparsed = isUnparsed; + DefaultValue = defaultValue; + } + + public async Task Parse(IMessage context, string input) + { + return await _reader.Read(context, input).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsUnparsed ? " (Unparsed)" : "")}"; + } +} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs new file mode 100644 index 000000000..e683d1de9 --- /dev/null +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -0,0 +1,144 @@ +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal static class CommandParser + { + private enum ParserPart + { + None, + Parameter, + QuotedParameter + } + + //TODO: Check support for escaping + public static async Task ParseArgs(Command command, IMessage context, string input, int startPos) + { + CommandParameter curParam = null; + StringBuilder argBuilder = new StringBuilder(input.Length); + int endPos = input.Length; + var curPart = ParserPart.None; + int lastArgEndPos = int.MinValue; + var argList = ImmutableArray.CreateBuilder(); + bool isEscaping = false; + char c; + + for (int curPos = startPos; curPos <= endPos; curPos++) + { + if (curPos < endPos) + c = input[curPos]; + else + c = '\0'; + + //If this character is escaped, skip it + if (isEscaping) + { + if (curPos != endPos) + { + argBuilder.Append(c); + isEscaping = false; + continue; + } + } + //Are we escaping the next character? + if (c == '\\') + { + isEscaping = true; + continue; + } + + //If we're processing an unparsed parameter, ignore all other logic + if (curParam != null && curParam.IsUnparsed) + { + argBuilder.Append(c); + continue; + } + + //If we're not currently processing one, are we starting the next argument yet? + if (curPart == ParserPart.None) + { + if (char.IsWhiteSpace(c) || curPos == endPos) + continue; //Skip whitespace between arguments + else if (curPos == lastArgEndPos) + return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments."); + else + { + curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; + if (curParam.IsUnparsed) + { + argBuilder.Append(c); + continue; + } + if (c == '\"') + { + curPart = ParserPart.QuotedParameter; + continue; + } + curPart = ParserPart.Parameter; + } + } + + //Has this parameter ended yet? + string argString = null; + if (curPart == ParserPart.Parameter) + { + if (curPos == endPos || char.IsWhiteSpace(c)) + { + argString = argBuilder.ToString(); + lastArgEndPos = curPos; + } + else + argBuilder.Append(c); + } + else if (curPart == ParserPart.QuotedParameter) + { + if (c == '\"') + { + argString = argBuilder.ToString(); //Remove quotes + lastArgEndPos = curPos + 1; + } + else + argBuilder.Append(c); + } + + if (argString != null) + { + if (curParam == null) + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + + var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); + if (!typeReaderResult.IsSuccess) + return ParseResult.FromError(typeReaderResult); + argList.Add(typeReaderResult.Value); + + curParam = null; + curPart = ParserPart.None; + argBuilder.Clear(); + } + } + + if (curParam != null && curParam.IsUnparsed) + argList.Add(argBuilder.ToString()); + + if (isEscaping) + return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape."); + if (curPart == ParserPart.QuotedParameter) + return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); + + if (argList.Count < command.Parameters.Count) + { + for (int i = argList.Count; i < command.Parameters.Count; i++) + { + var param = command.Parameters[i]; + if (!param.IsOptional) + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); + argList.Add(param.DefaultValue); + } + } + + return ParseResult.FromSuccess(argList.ToImmutable()); + } + } +} diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 2b9555b73..6c089d262 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Reflection; using System.Threading; @@ -14,6 +15,7 @@ namespace Discord.Commands private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _modules; private readonly ConcurrentDictionary> _map; + private readonly Dictionary _typeReaders; public IEnumerable Modules => _modules.Select(x => x.Value); public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); @@ -23,6 +25,113 @@ namespace Discord.Commands _moduleLock = new SemaphoreSlim(1, 1); _modules = new ConcurrentDictionary(); _map = new ConcurrentDictionary>(); + _typeReaders = new Dictionary + { + [typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), + [typeof(byte)] = new GenericTypeReader((m, s) => + { + byte value; + if (byte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Byte")); + }), + [typeof(sbyte)] = new GenericTypeReader((m, s) => + { + sbyte value; + if (sbyte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse SByte")); + }), + [typeof(ushort)] = new GenericTypeReader((m, s) => + { + ushort value; + if (ushort.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt16")); + }), + [typeof(short)] = new GenericTypeReader((m, s) => + { + short value; + if (short.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int16")); + }), + [typeof(uint)] = new GenericTypeReader((m, s) => + { + uint value; + if (uint.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt32")); + }), + [typeof(int)] = new GenericTypeReader((m, s) => + { + int value; + if (int.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int32")); + }), + [typeof(ulong)] = new GenericTypeReader((m, s) => + { + ulong value; + if (ulong.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt64")); + }), + [typeof(long)] = new GenericTypeReader((m, s) => + { + long value; + if (long.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int64")); + }), + [typeof(float)] = new GenericTypeReader((m, s) => + { + float value; + if (float.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Single")); + }), + [typeof(double)] = new GenericTypeReader((m, s) => + { + double value; + if (double.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Double")); + }), + [typeof(decimal)] = new GenericTypeReader((m, s) => + { + decimal value; + if (decimal.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Decimal")); + }), + [typeof(DateTime)] = new GenericTypeReader((m, s) => + { + DateTime value; + if (DateTime.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTime")); + }), + [typeof(DateTimeOffset)] = new GenericTypeReader((m, s) => + { + DateTimeOffset value; + if (DateTimeOffset.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTimeOffset")); + }), + + [typeof(IMessage)] = new MessageTypeReader(), + [typeof(IChannel)] = new ChannelTypeReader(), + [typeof(IGuildChannel)] = new ChannelTypeReader(), + [typeof(ITextChannel)] = new ChannelTypeReader(), + [typeof(IVoiceChannel)] = new ChannelTypeReader(), + [typeof(IRole)] = new RoleTypeReader(), + [typeof(IUser)] = new UserTypeReader(), + [typeof(IGuildUser)] = new UserTypeReader() + }; + } + + public void AddTypeReader(TypeReader reader) + { + _typeReaders[typeof(T)] = reader; + } + public void AddTypeReader(Type type, TypeReader reader) + { + _typeReaders[type] = reader; + } + internal TypeReader GetTypeReader(Type type) + { + TypeReader reader; + if (_typeReaders.TryGetValue(type, out reader)) + return reader; + return null; } public async Task Load(object module) @@ -46,7 +155,7 @@ namespace Discord.Commands } private Module LoadInternal(object module, TypeInfo typeInfo) { - var loadedModule = new Module(module, typeInfo); + var loadedModule = new Module(this, module, typeInfo); _modules[module] = loadedModule; foreach (var cmd in loadedModule.Commands) @@ -114,7 +223,7 @@ namespace Discord.Commands } //TODO: C#7 Candidate for tuple - public SearchResults Search(string input) + public SearchResult Search(string input) { string lowerInput = input.ToLowerInvariant(); @@ -125,21 +234,25 @@ namespace Discord.Commands { endPos = input.IndexOf(' ', startPos); string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); - startPos = endPos + 1; if (!_map.TryGetValue(cmdText, out group)) break; bestGroup = group; + if (endPos == -1) + { + startPos = input.Length; + break; + } + else + startPos = endPos + 1; } - - ImmutableArray cmds; + if (bestGroup != null) { lock (bestGroup) - cmds = bestGroup.ToImmutableArray(); + return SearchResult.FromSuccess(bestGroup.ToImmutableArray(), input.Substring(startPos)); } else - cmds = ImmutableArray.Create(); - return new SearchResults(cmds, startPos); + return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } } } diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/Module.cs index 230b3abd7..c2a7d4280 100644 --- a/src/Discord.Net.Commands/Module.cs +++ b/src/Discord.Net.Commands/Module.cs @@ -1,27 +1,33 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; namespace Discord.Commands { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Module { + public CommandService Service { get; } public string Name { get; } public IEnumerable Commands { get; } - internal Module(object parent, TypeInfo typeInfo) + internal Module(CommandService service, object instance, TypeInfo typeInfo) { + Service = service; + Name = typeInfo.Name; + List commands = new List(); - SearchClass(parent, commands, typeInfo); + SearchClass(instance, commands, typeInfo); Commands = commands; } - private void SearchClass(object parent, List commands, TypeInfo typeInfo) + private void SearchClass(object instance, List commands, TypeInfo typeInfo) { foreach (var method in typeInfo.DeclaredMethods) { var cmdAttr = method.GetCustomAttribute(); if (cmdAttr != null) - commands.Add(new Command(cmdAttr, method)); + commands.Add(new Command(this, instance, cmdAttr, method)); } foreach (var type in typeInfo.DeclaredNestedTypes) { @@ -29,5 +35,8 @@ namespace Discord.Commands SearchClass(ReflectionUtils.CreateObject(type), commands, type); } } + + public override string ToString() => Name; + private string DebuggerDisplay => Name; } } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs new file mode 100644 index 000000000..4a1350fee --- /dev/null +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class ChannelTypeReader : TypeReader + where T : class, IChannel + { + public override async Task Read(IMessage context, string input) + { + IGuildChannel guildChannel = context.Channel as IGuildChannel; + IChannel result = null; + + if (guildChannel != null) + { + //By Id + ulong id; + if (MentionUtils.TryParseChannel(input, out id) || ulong.TryParse(input, out id)) + { + var channel = await guildChannel.Guild.GetChannelAsync(id).ConfigureAwait(false); + if (channel != null) + result = channel; + } + + //By Name + if (result == null) + { + var channels = await guildChannel.Guild.GetChannelsAsync().ConfigureAwait(false); + var filteredChannels = channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (filteredChannels.Length > 1) + return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple channels found."); + else if (filteredChannels.Length == 1) + result = filteredChannels[0]; + } + } + + if (result == null) + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); + + T castResult = result as T; + if (castResult == null) + return TypeReaderResult.FromError(CommandError.CastFailed, $"Channel is not a {typeof(T).Name}."); + else + return TypeReaderResult.FromSuccess(castResult); + } + } +} diff --git a/src/Discord.Net.Commands/Readers/GenericTypeReader.cs b/src/Discord.Net.Commands/Readers/GenericTypeReader.cs new file mode 100644 index 000000000..97bc7d94c --- /dev/null +++ b/src/Discord.Net.Commands/Readers/GenericTypeReader.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class GenericTypeReader : TypeReader + { + private readonly Func> _action; + + public GenericTypeReader(Func> action) + { + _action = action; + } + + public override Task Read(IMessage context, string input) => _action(context, input); + } +} diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs new file mode 100644 index 000000000..50ec7000a --- /dev/null +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -0,0 +1,24 @@ +using System.Globalization; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class MessageTypeReader : TypeReader + { + public override Task Read(IMessage context, string input) + { + //By Id + ulong id; + if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var msg = context.Channel.GetCachedMessage(id); + if (msg == null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.")); + else + return Task.FromResult(TypeReaderResult.FromSuccess(msg)); + } + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Message Id.")); + } + } +} diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs new file mode 100644 index 000000000..10aee6b1c --- /dev/null +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class RoleTypeReader : TypeReader + { + public override Task Read(IMessage context, string input) + { + IGuildChannel guildChannel = context.Channel as IGuildChannel; + + if (guildChannel != null) + { + //By Id + ulong id; + if (MentionUtils.TryParseRole(input, out id) || ulong.TryParse(input, out id)) + { + var channel = guildChannel.Guild.GetRole(id); + if (channel != null) + return Task.FromResult(TypeReaderResult.FromSuccess(channel)); + } + + //By Name + var roles = guildChannel.Guild.Roles; + var filteredRoles = roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (filteredRoles.Length > 1) + return Task.FromResult(TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple roles found.")); + else if (filteredRoles.Length == 1) + return Task.FromResult(TypeReaderResult.FromSuccess(filteredRoles[0])); + } + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); + } + } +} diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs new file mode 100644 index 000000000..d1dedd9c8 --- /dev/null +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public abstract class TypeReader + { + public abstract Task Read(IMessage context, string input); + } +} diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs new file mode 100644 index 000000000..c80ac2816 --- /dev/null +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal class UserTypeReader : TypeReader + where T : class, IUser + { + public override async Task Read(IMessage context, string input) + { + IGuildChannel guildChannel = context.Channel as IGuildChannel; + IUser result = null; + + if (guildChannel != null) + { + //By Id + ulong id; + if (MentionUtils.TryParseUser(input, out id) || ulong.TryParse(input, out id)) + { + var user = await guildChannel.Guild.GetUserAsync(id).ConfigureAwait(false); + if (user != null) + result = user; + } + + //By Username + Discriminator + if (result == null) + { + int index = input.LastIndexOf('#'); + if (index >= 0) + { + string username = input.Substring(0, index); + ushort discriminator; + if (ushort.TryParse(input.Substring(index + 1), out discriminator)) + { + var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); + result = users.Where(x => + x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + } + } + } + + //By Username + if (result == null) + { + var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); + var filteredUsers = users.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (filteredUsers.Length > 1) + return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple users found."); + else if (filteredUsers.Length == 1) + result = filteredUsers[0]; + } + } + + if (result == null) + return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); + + T castResult = result as T; + if (castResult == null) + return TypeReaderResult.FromError(CommandError.CastFailed, $"User is not a {typeof(T).Name}."); + else + return TypeReaderResult.FromSuccess(castResult); + } + } +} diff --git a/src/Discord.Net.Commands/Results/ExecuteResult.cs b/src/Discord.Net.Commands/Results/ExecuteResult.cs new file mode 100644 index 000000000..a06e8dd99 --- /dev/null +++ b/src/Discord.Net.Commands/Results/ExecuteResult.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct ExecuteResult : IResult + { + public Exception Exception { get; } + + public CommandError? Error { get; } + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private ExecuteResult(Exception exception, CommandError? error, string errorReason) + { + Exception = exception; + Error = error; + ErrorReason = errorReason; + } + + internal static ExecuteResult FromSuccess() + => new ExecuteResult(null, null, null); + internal static ExecuteResult FromError(CommandError error, string reason) + => new ExecuteResult(null, error, reason); + internal static ExecuteResult FromError(Exception ex) + => new ExecuteResult(ex, CommandError.Exception, ex.Message); + internal static ExecuteResult FromError(ParseResult result) + => new ExecuteResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/Results/IResult.cs b/src/Discord.Net.Commands/Results/IResult.cs new file mode 100644 index 000000000..928d1139e --- /dev/null +++ b/src/Discord.Net.Commands/Results/IResult.cs @@ -0,0 +1,9 @@ +namespace Discord.Commands +{ + public interface IResult + { + CommandError? Error { get; } + string ErrorReason { get; } + bool IsSuccess { get; } + } +} diff --git a/src/Discord.Net.Commands/Results/ParseResult.cs b/src/Discord.Net.Commands/Results/ParseResult.cs new file mode 100644 index 000000000..e7e886b1a --- /dev/null +++ b/src/Discord.Net.Commands/Results/ParseResult.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct ParseResult : IResult + { + public IReadOnlyList Values { get; } + + public CommandError? Error { get; } + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private ParseResult(IReadOnlyList values, CommandError? error, string errorReason) + { + Values = values; + Error = error; + ErrorReason = errorReason; + } + + internal static ParseResult FromSuccess(IReadOnlyList values) + => new ParseResult(values, null, null); + internal static ParseResult FromError(CommandError error, string reason) + => new ParseResult(null, error, reason); + internal static ParseResult FromError(SearchResult result) + => new ParseResult(null, result.Error, result.ErrorReason); + internal static ParseResult FromError(TypeReaderResult result) + => new ParseResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({Values.Count} Values)" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs new file mode 100644 index 000000000..0c7d671e3 --- /dev/null +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct SearchResult : IResult + { + public IReadOnlyList Commands { get; } + public string ArgText { get; } + + public CommandError? Error { get; } + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private SearchResult(IReadOnlyList commands, string argText, CommandError? error, string errorReason) + { + Commands = commands; + ArgText = argText; + Error = error; + ErrorReason = errorReason; + } + + internal static SearchResult FromSuccess(IReadOnlyList commands, string argText) + => new SearchResult(commands, argText, null, null); + internal static SearchResult FromError(CommandError error, string reason) + => new SearchResult(null, null, error, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs new file mode 100644 index 000000000..932f1299b --- /dev/null +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public struct TypeReaderResult : IResult + { + public object Value { get; } + + public CommandError? Error { get; } + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private TypeReaderResult(object value, CommandError? error, string errorReason) + { + Value = value; + Error = error; + ErrorReason = errorReason; + } + + public static TypeReaderResult FromSuccess(object value) + => new TypeReaderResult(value, null, null); + public static TypeReaderResult FromError(CommandError error, string reason) + => new TypeReaderResult(null, error, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? $"Success ({Value})" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/SearchResults.cs b/src/Discord.Net.Commands/SearchResults.cs deleted file mode 100644 index 724b61ecc..000000000 --- a/src/Discord.Net.Commands/SearchResults.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.Commands -{ - public struct SearchResults - { - IReadOnlyList Commands { get; } - int ArgsPos { get; } - - public SearchResults(IReadOnlyList commands, int argsPos) - { - Commands = commands; - ArgsPos = argsPos; - } - } -} From 21102e460c4fab0f23ea61f01c44bcaa2246c7fc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 05:08:29 -0300 Subject: [PATCH 120/160] Fixed null char being appended to unparsed args --- src/Discord.Net.Commands/CommandParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index e683d1de9..cd23cbbc3 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -50,7 +50,7 @@ namespace Discord.Commands } //If we're processing an unparsed parameter, ignore all other logic - if (curParam != null && curParam.IsUnparsed) + if (curParam != null && curParam.IsUnparsed && curPos != endPos) { argBuilder.Append(c); continue; From 851589f7db288287578c9fa165135493c1c04551 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 17:06:18 -0300 Subject: [PATCH 121/160] Dont error if an uncached message is delete --- src/Discord.Net/DiscordSocketClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 35891b1b6..966b525c5 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -948,7 +948,10 @@ namespace Discord if (channel != null) { var msg = channel.RemoveMessage(data.Id); - await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create(msg)).ConfigureAwait(false); + if (msg != null) + await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create(msg)).ConfigureAwait(false); + else + await _messageDeletedEvent.InvokeAsync(data.Id, Optional.Create()).ConfigureAwait(false); } else { @@ -968,7 +971,10 @@ namespace Discord foreach (var id in data.Ids) { var msg = channel.RemoveMessage(id); - await _messageDeletedEvent.InvokeAsync(msg.Id, Optional.Create(msg)).ConfigureAwait(false); + if (msg != null) + await _messageDeletedEvent.InvokeAsync(id, Optional.Create(msg)).ConfigureAwait(false); + else + await _messageDeletedEvent.InvokeAsync(id, Optional.Create()).ConfigureAwait(false); } } else From bfea0ef0a2aec7a9f67bd0d8d6ce2b92059a75d4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 19:06:10 -0300 Subject: [PATCH 122/160] Added ordered user mentions, fixed role and channel mention resolving --- .../Results/TypeReaderResult.cs | 1 + src/Discord.Net/Entities/Messages/IMessage.cs | 6 +- src/Discord.Net/Entities/Messages/Message.cs | 50 ++++++++------- src/Discord.Net/Utilities/MentionUtils.cs | 62 ++++++++++++------- 4 files changed, 69 insertions(+), 50 deletions(-) diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index 932f1299b..beeabab16 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.InteropServices; namespace Discord.Commands { diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 3a91336d2..61f1199eb 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -30,9 +30,9 @@ namespace Discord IReadOnlyCollection Embeds { get; } /// Returns a collection of channel ids mentioned in this message. IReadOnlyCollection MentionedChannelIds { get; } - /// Returns a collection of role ids mentioned in this message. - IReadOnlyCollection MentionedRoleIds { get; } - /// Returns a collection of user ids mentioned in this message. + /// Returns a collection of roles mentioned in this message. + IReadOnlyCollection MentionedRoles { get; } + /// Returns a collection of users mentioned in this message. IReadOnlyCollection MentionedUsers { get; } /// Modifies this message. diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index bd91de78a..e0fba96cb 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -23,11 +23,11 @@ namespace Discord public IMessageChannel Channel { get; } public IUser Author { get; } - public ImmutableArray Attachments { get; private set; } - public ImmutableArray Embeds { get; private set; } - public ImmutableArray MentionedChannelIds { get; private set; } - public ImmutableArray MentionedRoleIds { get; private set; } - public ImmutableArray MentionedUsers { get; private set; } + public IReadOnlyCollection Attachments { get; private set; } + public IReadOnlyCollection Embeds { get; private set; } + public IReadOnlyCollection MentionedChannelIds { get; private set; } + public IReadOnlyCollection MentionedRoles { get; private set; } + public IReadOnlyCollection MentionedUsers { get; private set; } public override DiscordClient Discord => (Channel as Entity).Discord; public DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); @@ -41,9 +41,9 @@ namespace Discord if (channel is IGuildChannel) { - MentionedUsers = ImmutableArray.Create(); + MentionedUsers = ImmutableArray.Create(); MentionedChannelIds = ImmutableArray.Create(); - MentionedRoleIds = ImmutableArray.Create(); + MentionedRoles = ImmutableArray.Create(); } Update(model, UpdateSource.Creation); @@ -106,21 +106,31 @@ namespace Discord MentionedUsers = ImmutableArray.Create(mentions); } else - MentionedUsers = ImmutableArray.Create(); + MentionedUsers = ImmutableArray.Create(); } if (model.Content.IsSpecified) { RawText = model.Content.Value; - - if (Channel is IGuildChannel) + + if (guildChannel != null) { - Text = MentionUtils.CleanUserMentions(RawText, MentionedUsers); - MentionedChannelIds = MentionUtils.GetChannelMentions(RawText); - var mentionedRoleIds = MentionUtils.GetRoleMentions(RawText); - if (_isMentioningEveryone) - mentionedRoleIds = mentionedRoleIds.Add(guildChannel.Guild.EveryoneRole.Id); - MentionedRoleIds = mentionedRoleIds; + var orderedMentionedUsers = ImmutableArray.CreateBuilder(5); + Text = MentionUtils.CleanUserMentions(RawText, Channel.IsAttached ? Channel : null, MentionedUsers, orderedMentionedUsers); + MentionedUsers = orderedMentionedUsers.ToImmutable(); + + var roles = ImmutableArray.CreateBuilder(5); + Text = MentionUtils.CleanRoleMentions(Text, guildChannel.Guild, roles); + MentionedRoles = roles.ToImmutable(); + + if (guildChannel.IsAttached) //It's too expensive to do a channel lookup in REST mode + { + var channelIds = ImmutableArray.CreateBuilder(5); + Text = MentionUtils.CleanChannelMentions(Text, guildChannel.Guild, channelIds); + MentionedChannelIds = channelIds.ToImmutable(); + } + else + MentionedChannelIds = MentionUtils.GetChannelMentions(RawText); } else Text = RawText; @@ -172,12 +182,6 @@ namespace Discord } public override string ToString() => Text; - private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Length > 0 ? $" [{Attachments.Length} Attachments]" : "")}"; - - IReadOnlyCollection IMessage.Attachments => Attachments; - IReadOnlyCollection IMessage.Embeds => Embeds; - IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannelIds; - IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoleIds; - IReadOnlyCollection IMessage.MentionedUsers => MentionedUsers; + private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; } } diff --git a/src/Discord.Net/Utilities/MentionUtils.cs b/src/Discord.Net/Utilities/MentionUtils.cs index 1a092fd9c..f348d4962 100644 --- a/src/Discord.Net/Utilities/MentionUtils.cs +++ b/src/Discord.Net/Utilities/MentionUtils.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace Discord { @@ -107,7 +108,7 @@ namespace Discord return builder; } - internal static string CleanUserMentions(string text, ImmutableArray mentions) + /*internal static string CleanUserMentions(string text, ImmutableArray mentions) { return _userRegex.Replace(text, new MatchEvaluator(e => { @@ -123,58 +124,71 @@ namespace Discord } return e.Value; })); - } - internal static string CleanUserMentions(string text, IReadOnlyDictionary users, ImmutableArray.Builder mentions = null) - where T : IGuildUser + }*/ + internal static string CleanUserMentions(string text, IMessageChannel channel, IReadOnlyCollection fallbackUsers, ImmutableArray.Builder mentions = null) { - return _channelRegex.Replace(text, new MatchEvaluator(e => + return _userRegex.Replace(text, new MatchEvaluator(e => { ulong id; if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - T user; - if (users.TryGetValue(id, out user)) + IUser user = null; + if (channel != null) + user = channel.GetUserAsync(id).GetAwaiter().GetResult() as IUser; + if (user == null) + { + foreach (var fallbackUser in fallbackUsers) + { + if (fallbackUser.Id == id) + { + user = fallbackUser; + break; + } + } + } + if (user != null) { - if (users != null) - mentions.Add(user); - if (e.Value[2] == '!' && user.Nickname != null) - return '@' + user.Nickname; - else - return '@' + user.Username; + mentions.Add(user); + + if (e.Value[2] == '!') + { + var guildUser = user as IGuildUser; + if (guildUser != null && guildUser.Nickname != null) + return '@' + guildUser.Nickname; + } + return '@' + user.Username; } } return e.Value; })); } - internal static string CleanChannelMentions(string text, IReadOnlyDictionary channels, ImmutableArray.Builder mentions = null) - where T : IGuildChannel + internal static string CleanChannelMentions(string text, IGuild guild, ImmutableArray.Builder mentions = null) { return _channelRegex.Replace(text, new MatchEvaluator(e => { ulong id; if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - T channel; - if (channels.TryGetValue(id, out channel)) + var channel = guild.GetChannelAsync(id).GetAwaiter().GetResult() as IGuildChannel; + if (channel != null) { - if (channels != null) - mentions.Add(channel); + if (mentions != null) + mentions.Add(channel.Id); return '#' + channel.Name; } } return e.Value; })); } - internal static string CleanRoleMentions(string text, IReadOnlyDictionary roles, ImmutableArray.Builder mentions = null) - where T : IRole + internal static string CleanRoleMentions(string text, IGuild guild, ImmutableArray.Builder mentions = null) { - return _channelRegex.Replace(text, new MatchEvaluator(e => + return _roleRegex.Replace(text, new MatchEvaluator(e => { ulong id; if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - T role; - if (roles.TryGetValue(id, out role)) + var role = guild.GetRole(id); + if (role != null) { if (mentions != null) mentions.Add(role); From f6595aa73e54bafd7e9206aba6ef912cc27caa41 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 19:47:53 -0300 Subject: [PATCH 123/160] Added IDisposable to DiscordClients, removed unused property --- src/Discord.Net/API/DiscordAPIClient.cs | 2 ++ src/Discord.Net/DiscordClient.cs | 6 +++--- src/Discord.Net/DiscordSocketClient.cs | 4 +--- src/Discord.Net/IDiscordClient.cs | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index c138e455f..2a0b4c02e 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -99,6 +99,8 @@ namespace Discord.API { _loginCancelToken?.Dispose(); _connectCancelToken?.Dispose(); + (_restClient as IDisposable)?.Dispose(); + (_gatewayClient as IDisposable)?.Dispose(); } _isDisposed = true; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index bc5601bb1..9b0cfbb6b 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,5 +1,4 @@ using Discord.API.Rest; -using Discord.Extensions; using Discord.Logging; using Discord.Net; using Discord.Net.Queue; @@ -56,7 +55,7 @@ namespace Discord if (bucket == null && id != null) await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); }; - + ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } @@ -256,10 +255,11 @@ namespace Discord return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); } - internal void Dispose(bool disposing) + internal virtual void Dispose(bool disposing) { if (!_isDisposed) _isDisposed = true; + ApiClient.Dispose(); } /// public void Dispose() => Dispose(true); diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 966b525c5..f27a26657 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -47,8 +47,7 @@ namespace Discord public ConnectionState ConnectionState { get; private set; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } - - internal IWebSocketClient GatewaySocket { get; private set; } + internal int MessageCacheSize { get; private set; } internal DataStore DataStore { get; private set; } @@ -97,7 +96,6 @@ namespace Discord else await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; - GatewaySocket = config.WebSocketProvider(); _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 821de976f..c65d7b49e 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,4 +1,5 @@ using Discord.API; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -6,7 +7,7 @@ using System.Threading.Tasks; namespace Discord { //TODO: Add docstrings - public interface IDiscordClient + public interface IDiscordClient : IDisposable { LoginState LoginState { get; } ConnectionState ConnectionState { get; } From 5ac320312d22c67cd5992f405968a48250264301 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 21:00:39 -0300 Subject: [PATCH 124/160] Added CommandService.Unload(Module) overload --- src/Discord.Net.Commands/CommandService.cs | 12 ++++++++++++ src/Discord.Net.Commands/Module.cs | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 6c089d262..01613a3a3 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -190,6 +190,18 @@ namespace Discord.Commands } } + public async Task Unload(Module module) + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + return UnloadInternal(module.Instance); + } + finally + { + _moduleLock.Release(); + } + } public async Task Unload(object module) { await _moduleLock.WaitAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/Module.cs index c2a7d4280..0f7edb551 100644 --- a/src/Discord.Net.Commands/Module.cs +++ b/src/Discord.Net.Commands/Module.cs @@ -10,11 +10,13 @@ namespace Discord.Commands public CommandService Service { get; } public string Name { get; } public IEnumerable Commands { get; } + internal object Instance { get; } internal Module(CommandService service, object instance, TypeInfo typeInfo) { Service = service; Name = typeInfo.Name; + Instance = instance; List commands = new List(); SearchClass(instance, commands, typeInfo); From eb7ec637a570d89e8abc7fd56026908776af777c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 21:06:34 -0300 Subject: [PATCH 125/160] Cleaned up several CommandService functions --- src/Discord.Net.Commands/CommandService.cs | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 01613a3a3..ec96479bb 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -134,29 +134,29 @@ namespace Discord.Commands return null; } - public async Task Load(object module) + public async Task Load(object moduleInstance) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - if (_modules.ContainsKey(module)) + if (_modules.ContainsKey(moduleInstance)) throw new ArgumentException($"This module has already been loaded."); - var typeInfo = module.GetType().GetTypeInfo(); + var typeInfo = moduleInstance.GetType().GetTypeInfo(); if (typeInfo.GetCustomAttribute() == null) throw new ArgumentException($"Modules must be marked with ModuleAttribute."); - return LoadInternal(module, typeInfo); + return LoadInternal(moduleInstance, typeInfo); } finally { _moduleLock.Release(); } } - private Module LoadInternal(object module, TypeInfo typeInfo) + private Module LoadInternal(object moduleInstance, TypeInfo typeInfo) { - var loadedModule = new Module(this, module, typeInfo); - _modules[module] = loadedModule; + var loadedModule = new Module(this, moduleInstance, typeInfo); + _modules[moduleInstance] = loadedModule; foreach (var cmd in loadedModule.Commands) { @@ -169,7 +169,7 @@ namespace Discord.Commands } public async Task> LoadAssembly(Assembly assembly) { - List modules = new List(); + var modules = ImmutableArray.CreateBuilder(); await _moduleLock.WaitAsync().ConfigureAwait(false); try { @@ -178,11 +178,11 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.GetCustomAttribute() != null) { - var module = ReflectionUtils.CreateObject(typeInfo); - modules.Add(LoadInternal(module, typeInfo)); + var moduleInstance = ReflectionUtils.CreateObject(typeInfo); + modules.Add(LoadInternal(moduleInstance, typeInfo)); } } - return modules; + return modules.ToImmutable(); } finally { @@ -202,12 +202,12 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task Unload(object module) + public async Task Unload(object moduleInstance) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return UnloadInternal(module); + return UnloadInternal(moduleInstance); } finally { @@ -233,8 +233,7 @@ namespace Discord.Commands else return false; } - - //TODO: C#7 Candidate for tuple + public SearchResult Search(string input) { string lowerInput = input.ToLowerInvariant(); From 0e710cc76aec7ee053f1ca8f2b9ccc85e604a8b6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 21:40:05 -0300 Subject: [PATCH 126/160] Added a shorthand CommandService.Execute method --- src/Discord.Net.Commands/CommandService.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index ec96479bb..a2f5c7004 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -265,5 +265,24 @@ namespace Discord.Commands else return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } + + public async Task Execute(IMessage message, string input) + { + var searchResult = Search(input); + if (!searchResult.IsSuccess) + return searchResult; + + var commands = searchResult.Commands; + for (int i = 0; i < commands.Count; i++) + { + var parseResult = await commands[i].Parse(message, searchResult); + if (!parseResult.IsSuccess) + continue; + var executeResult = await commands[i].Execute(message, parseResult); + return executeResult; + } + + return ParseResult.FromError(CommandError.ParseFailed, "This input does not match any overload."); + } } } From e7597631d234f930e37e3c97ea2abc35f3e78ba9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 21:42:29 -0300 Subject: [PATCH 127/160] Added NicknameMention to IUser --- src/Discord.Net/Entities/Users/GuildUser.cs | 1 + src/Discord.Net/Entities/Users/IUser.cs | 2 ++ src/Discord.Net/Entities/WebSocket/CachedDMUser.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index 31ea9f01c..5a9e19278 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -31,6 +31,7 @@ namespace Discord public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; + public string NicknameMention => User.NicknameMention; public string Username => User.Username; public virtual UserStatus Status => UserStatus.Unknown; diff --git a/src/Discord.Net/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs index 5eef8231c..9e78781d2 100644 --- a/src/Discord.Net/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -12,5 +12,7 @@ namespace Discord bool IsBot { get; } /// Gets the username for this user. string Username { get; } + /// Returns a special string used to mention this object, by nickname. + string NicknameMention { get; } } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs index 78d78933f..cddd91c4f 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs @@ -20,6 +20,7 @@ namespace Discord public bool IsAttached => User.IsAttached; public bool IsBot => User.IsBot; public string Mention => User.Mention; + public string NicknameMention => User.NicknameMention; public string Username => User.Username; public CachedDMUser(CachedGlobalUser user) From abeadce5cbabd53ec5428d6deb3ac86755603f6d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 22:36:41 -0300 Subject: [PATCH 128/160] Added project for audio --- src/Discord.Net.Audio/Discord.Net.Audio.xproj | 19 +++++++++++ src/Discord.Net.Audio/project.json | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/Discord.Net.Audio/Discord.Net.Audio.xproj create mode 100644 src/Discord.Net.Audio/project.json diff --git a/src/Discord.Net.Audio/Discord.Net.Audio.xproj b/src/Discord.Net.Audio/Discord.Net.Audio.xproj new file mode 100644 index 000000000..9f9783c6e --- /dev/null +++ b/src/Discord.Net.Audio/Discord.Net.Audio.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25123 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ddfcc44f-934e-478a-978c-69cdda2a1c5b + Discord.Net.Audio + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json new file mode 100644 index 000000000..64992a775 --- /dev/null +++ b/src/Discord.Net.Audio/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0-dev", + "description": "A Discord.Net extension adding audio support.", + "authors": [ "RogueException" ], + + "packOptions": { + "tags": [ "discord", "discordapp" ], + "licenseUrl": "http://opensource.org/licenses/MIT", + "projectUrl": "https://github.com/RogueException/Discord.Net", + "repository": { + "type": "git", + "url": "git://github.com/RogueException/Discord.Net" + } + }, + + "buildOptions": { + "allowUnsafe": true, + "warningsAsErrors": false + }, + + "dependencies": { + "Discord.Net": "1.0.0-dev" + }, + + "frameworks": { + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } + } +} From f6c31a0b19f32ce9969c0aa785e44fcc468df508 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 22:53:46 -0300 Subject: [PATCH 129/160] Added argPos overloadeds to CommandService's Search and Execute --- src/Discord.Net.Commands/CommandService.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index a2f5c7004..a4b4f8679 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -233,8 +233,9 @@ namespace Discord.Commands else return false; } - - public SearchResult Search(string input) + + public SearchResult Search(IMessage message, int argPos) => Search(message, message.RawText.Substring(argPos)); + public SearchResult Search(IMessage message, string input) { string lowerInput = input.ToLowerInvariant(); @@ -266,9 +267,10 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } + public Task Execute(IMessage message, int argPos) => Execute(message, message.RawText.Substring(argPos)); public async Task Execute(IMessage message, string input) { - var searchResult = Search(input); + var searchResult = Search(message, input); if (!searchResult.IsSuccess) return searchResult; From f76d4685827e2bd07807317471cf04c21be02f52 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 22:54:01 -0300 Subject: [PATCH 130/160] Dont initialize the websocket engine if DiscordClient is given a DiscordSocketConfig --- src/Discord.Net/DiscordClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 9b0cfbb6b..3e87dee1e 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -56,7 +56,9 @@ namespace Discord await _queueLogger.WarningAsync($"Unknown rate limit bucket \"{id ?? "null"}\"").ConfigureAwait(false); }; - ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); + var restProvider = config.RestClientProvider; + var webSocketProvider = (this is DiscordSocketClient) ? (config as DiscordSocketConfig)?.WebSocketProvider : null; //TODO: Clean this check + ApiClient = new API.DiscordApiClient(restProvider, webSocketProvider, requestQueue: _requestQueue); ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } From 602ac134e5c88f200815736573162145b7134b74 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 22:54:12 -0300 Subject: [PATCH 131/160] Added Prefix check extensions to IMessage --- .../Extensions/MessageExtensions.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Discord.Net.Commands/Extensions/MessageExtensions.cs diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs new file mode 100644 index 000000000..2ba4973fe --- /dev/null +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -0,0 +1,44 @@ +namespace Discord.Commands +{ + public static class MessageExtensions + { + public static bool HasCharPrefix(this IMessage msg, char c, ref int argPos) + { + var text = msg.RawText; + if (text.Length > 0 && text[0] == c) + { + argPos = 1; + return true; + } + return false; + } + public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) + { + var text = msg.RawText; + str = str + ' '; + if (text.StartsWith(str)) + { + argPos = str.Length; + return true; + } + return false; + } + public static bool HasMentionPrefix(this IMessage msg, IUser user, ref int argPos) + { + var text = msg.RawText; + string mention = user.Mention + ' '; + if (text.StartsWith(mention)) + { + argPos = mention.Length; + return true; + } + string nickMention = user.NicknameMention + ' '; + if (text.StartsWith(mention)) + { + argPos = nickMention.Length; + return true; + } + return false; + } + } +} \ No newline at end of file From 9365a70e1d605aa088cc78236eee8a5b718320e4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 26 Jun 2016 23:03:05 -0300 Subject: [PATCH 132/160] Added IGuildUser Add/Remove Roles extensions --- .../Extensions/GuildUserExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Discord.Net/Extensions/GuildUserExtensions.cs diff --git a/src/Discord.Net/Extensions/GuildUserExtensions.cs b/src/Discord.Net/Extensions/GuildUserExtensions.cs new file mode 100644 index 000000000..9575e66dc --- /dev/null +++ b/src/Discord.Net/Extensions/GuildUserExtensions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Extensions +{ + public static class GuildUserExtensions + { + public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) + => AddRolesAsync(user, (IEnumerable)roles); + public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) + => user.ModifyAsync(x => x.Roles = Optional.Create(user.Roles.Concat(roles))); + + public static Task RemoveRolesAsync(this IGuildUser user, params IRole[] roles) + => RemoveRolesAsync(user, (IEnumerable)roles); + public static Task RemoveRolesAsync(this IGuildUser user, IEnumerable roles) + => user.ModifyAsync(x => x.Roles = Optional.Create(user.Roles.Except(roles))); + } +} From 6300a24eb5d8fe0b8766e145db4935f4ac9c34cc Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 06:56:02 -0300 Subject: [PATCH 133/160] Added note about username rate limit --- src/Discord.Net/Net/Queue/RequestQueue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net/Net/Queue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs index f23176411..826bdd5fa 100644 --- a/src/Discord.Net/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -32,6 +32,7 @@ namespace Discord.Net.Queue //[GlobalBucket.Login] = new BucketDefinition(1, 1), [GlobalBucket.DirectMessage] = new Bucket("bot:msg:dm", 5, 5, BucketTarget.Bot), [GlobalBucket.SendEditMessage] = new Bucket("bot:msg:global", 50, 10, BucketTarget.Bot), + //[GlobalBucket.Username] = new Bucket("bot:msg:global", 2, 3600, BucketTarget.Both), //Gateway [GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both), From 7bb890cbfe49eb93f4a775a6e4f79d60c608b0b9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 06:56:24 -0300 Subject: [PATCH 134/160] Added command groups, fixed several bugs --- .../Attributes/GroupAttribute.cs | 10 ++-- .../Attributes/ModuleAttribute.cs | 9 ++++ src/Discord.Net.Commands/Command.cs | 6 +-- src/Discord.Net.Commands/CommandParameter.cs | 1 + src/Discord.Net.Commands/CommandParser.cs | 2 +- src/Discord.Net.Commands/CommandService.cs | 52 +++++++++++-------- .../Extensions/MessageExtensions.cs | 2 +- src/Discord.Net.Commands/Module.cs | 22 +++++--- .../Results/SearchResult.cs | 10 ++-- 9 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index a39437862..3521f3f4f 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -5,10 +5,14 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class)] public class GroupAttribute : Attribute { - public string Name { get; } - public GroupAttribute(string name) + public string Prefix { get; } + public GroupAttribute() { - Name = name; + Prefix = null; + } + public GroupAttribute(string prefix) + { + Prefix = prefix; } } } diff --git a/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs b/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs index 5e9481a45..59e6a6aca 100644 --- a/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ModuleAttribute.cs @@ -5,5 +5,14 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class)] public class ModuleAttribute : Attribute { + public string Prefix { get; } + public ModuleAttribute() + { + Prefix = null; + } + public ModuleAttribute(string prefix) + { + Prefix = prefix; + } } } diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index d3b94b94c..53c4758d5 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -19,13 +19,13 @@ namespace Discord.Commands public Module Module { get; } public IReadOnlyList Parameters { get; } - internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo) + internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo, string groupPrefix) { Module = module; _instance = instance; Name = methodInfo.Name; - Text = attribute.Text; + Text = groupPrefix + attribute.Text; var description = methodInfo.GetCustomAttribute(); if (description != null) @@ -40,7 +40,7 @@ namespace Discord.Commands if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); - return await CommandParser.ParseArgs(this, msg, searchResult.ArgText, 0).ConfigureAwait(false); + return await CommandParser.ParseArgs(this, msg, searchResult.Text.Substring(Text.Length), 0).ConfigureAwait(false); } public async Task Execute(IMessage msg, ParseResult parseResult) { diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs index 0a5952c48..e5b272ebe 100644 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ b/src/Discord.Net.Commands/CommandParameter.cs @@ -18,6 +18,7 @@ namespace Discord.Commands public CommandParameter(string name, string description, TypeReader reader, bool isOptional, bool isUnparsed, object defaultValue) { _reader = reader; + Name = name; IsOptional = isOptional; IsUnparsed = isUnparsed; DefaultValue = defaultValue; diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index cd23cbbc3..262d3cb58 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -66,7 +66,7 @@ namespace Discord.Commands else { curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; - if (curParam.IsUnparsed) + if (curParam != null && curParam.IsUnparsed) { argBuilder.Append(c); continue; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index a4b4f8679..f3b623700 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -15,7 +15,7 @@ namespace Discord.Commands private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _modules; private readonly ConcurrentDictionary> _map; - private readonly Dictionary _typeReaders; + private readonly ConcurrentDictionary _typeReaders; public IEnumerable Modules => _modules.Select(x => x.Value); public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); @@ -25,7 +25,7 @@ namespace Discord.Commands _moduleLock = new SemaphoreSlim(1, 1); _modules = new ConcurrentDictionary(); _map = new ConcurrentDictionary>(); - _typeReaders = new Dictionary + _typeReaders = new ConcurrentDictionary { [typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), [typeof(byte)] = new GenericTypeReader((m, s) => @@ -143,19 +143,20 @@ namespace Discord.Commands throw new ArgumentException($"This module has already been loaded."); var typeInfo = moduleInstance.GetType().GetTypeInfo(); - if (typeInfo.GetCustomAttribute() == null) + var moduleAttr = typeInfo.GetCustomAttribute(); + if (moduleAttr != null) throw new ArgumentException($"Modules must be marked with ModuleAttribute."); - return LoadInternal(moduleInstance, typeInfo); + return LoadInternal(moduleInstance, moduleAttr, typeInfo); } finally { _moduleLock.Release(); } } - private Module LoadInternal(object moduleInstance, TypeInfo typeInfo) + private Module LoadInternal(object moduleInstance, ModuleAttribute moduleAttr, TypeInfo typeInfo) { - var loadedModule = new Module(this, moduleInstance, typeInfo); + var loadedModule = new Module(this, moduleInstance, moduleAttr, typeInfo); _modules[moduleInstance] = loadedModule; foreach (var cmd in loadedModule.Commands) @@ -176,10 +177,11 @@ namespace Discord.Commands foreach (var type in assembly.ExportedTypes) { var typeInfo = type.GetTypeInfo(); - if (typeInfo.GetCustomAttribute() != null) + var moduleAttr = typeInfo.GetCustomAttribute(); + if (moduleAttr != null) { var moduleInstance = ReflectionUtils.CreateObject(typeInfo); - modules.Add(LoadInternal(moduleInstance, typeInfo)); + modules.Add(LoadInternal(moduleInstance, moduleAttr, typeInfo)); } } return modules.ToImmutable(); @@ -239,30 +241,34 @@ namespace Discord.Commands { string lowerInput = input.ToLowerInvariant(); - List bestGroup = null, group; - int startPos = 0, endPos; + ImmutableArray.Builder matches = null; + List group; + int pos = -1; while (true) { - endPos = input.IndexOf(' ', startPos); - string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); + pos = input.IndexOf(' ', pos + 1); + string cmdText = pos == -1 ? input : input.Substring(0, pos); if (!_map.TryGetValue(cmdText, out group)) break; - bestGroup = group; - if (endPos == -1) + + lock (group) + { + if (matches == null) + matches = ImmutableArray.CreateBuilder(group.Count); + for (int i = 0; i < group.Count; i++) + matches.Add(group[i]); + } + + if (pos == -1) { - startPos = input.Length; + pos = input.Length; break; } - else - startPos = endPos + 1; } - if (bestGroup != null) - { - lock (bestGroup) - return SearchResult.FromSuccess(bestGroup.ToImmutableArray(), input.Substring(startPos)); - } + if (matches != null) + return SearchResult.FromSuccess(input, matches.ToImmutable()); else return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } @@ -275,7 +281,7 @@ namespace Discord.Commands return searchResult; var commands = searchResult.Commands; - for (int i = 0; i < commands.Count; i++) + for (int i = commands.Count - 1; i >= 0; i++) { var parseResult = await commands[i].Parse(message, searchResult); if (!parseResult.IsSuccess) diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 2ba4973fe..b2e3e173c 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -15,7 +15,7 @@ public static bool HasStringPrefix(this IMessage msg, string str, ref int argPos) { var text = msg.RawText; - str = str + ' '; + //str = str + ' '; if (text.StartsWith(str)) { argPos = str.Length; diff --git a/src/Discord.Net.Commands/Module.cs b/src/Discord.Net.Commands/Module.cs index 0f7edb551..ea6e29c28 100644 --- a/src/Discord.Net.Commands/Module.cs +++ b/src/Discord.Net.Commands/Module.cs @@ -12,29 +12,39 @@ namespace Discord.Commands public IEnumerable Commands { get; } internal object Instance { get; } - internal Module(CommandService service, object instance, TypeInfo typeInfo) + internal Module(CommandService service, object instance, ModuleAttribute moduleAttr, TypeInfo typeInfo) { Service = service; Name = typeInfo.Name; Instance = instance; List commands = new List(); - SearchClass(instance, commands, typeInfo); + SearchClass(instance, commands, typeInfo, moduleAttr.Prefix ?? ""); Commands = commands; } - private void SearchClass(object instance, List commands, TypeInfo typeInfo) + private void SearchClass(object instance, List commands, TypeInfo typeInfo, string groupPrefix) { + if (groupPrefix != "") + groupPrefix += " "; foreach (var method in typeInfo.DeclaredMethods) { var cmdAttr = method.GetCustomAttribute(); if (cmdAttr != null) - commands.Add(new Command(this, instance, cmdAttr, method)); + commands.Add(new Command(this, instance, cmdAttr, method, groupPrefix)); } foreach (var type in typeInfo.DeclaredNestedTypes) { - if (type.GetCustomAttribute() != null) - SearchClass(ReflectionUtils.CreateObject(type), commands, type); + var groupAttrib = type.GetCustomAttribute(); + if (groupAttrib != null) + { + string nextGroupPrefix; + if (groupAttrib.Prefix != null) + nextGroupPrefix = groupPrefix + groupAttrib.Prefix ?? type.Name; + else + nextGroupPrefix = groupPrefix; + SearchClass(ReflectionUtils.CreateObject(type), commands, type, nextGroupPrefix); + } } } diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs index 0c7d671e3..3cda94ba4 100644 --- a/src/Discord.Net.Commands/Results/SearchResult.cs +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -6,24 +6,24 @@ namespace Discord.Commands [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct SearchResult : IResult { + public string Text { get; } public IReadOnlyList Commands { get; } - public string ArgText { get; } public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private SearchResult(IReadOnlyList commands, string argText, CommandError? error, string errorReason) + private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) { + Text = text; Commands = commands; - ArgText = argText; Error = error; ErrorReason = errorReason; } - internal static SearchResult FromSuccess(IReadOnlyList commands, string argText) - => new SearchResult(commands, argText, null, null); + internal static SearchResult FromSuccess(string text, IReadOnlyList commands) + => new SearchResult(text, commands, null, null); internal static SearchResult FromError(CommandError error, string reason) => new SearchResult(null, null, error, reason); From 63fbdb394bc90595f2e9df6a7e387ede36662f18 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 07:07:56 -0300 Subject: [PATCH 135/160] Ensure Task return type for commands --- src/Discord.Net.Commands/Command.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 53c4758d5..f9578d154 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -81,7 +81,7 @@ namespace Discord.Commands var reader = Module.Service.GetTypeReader(type); if (reader == null) - throw new InvalidOperationException($"This type ({type.FullName}) is not supported."); + throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); bool isUnparsed = parameter.GetCustomAttribute() != null; if (isUnparsed) @@ -103,6 +103,9 @@ namespace Discord.Commands } private Func, Task> BuildAction(MethodInfo methodInfo) { + if (methodInfo.ReturnType != typeof(Task)) + throw new InvalidOperationException("Commands must return a non-generic Task."); + //TODO: Temporary reflection hack. Lets build an actual expression tree here. return (msg, args) => { From e649696332c714df876b17d670e103b61d902035 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 07:30:53 -0300 Subject: [PATCH 136/160] Added fancy badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11d40e881..247a05999 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Discord.Net v1.0.0-dev -[![Build status](https://ci.appveyor.com/api/projects/status/p0n69xhqgmoobycf/branch/master?svg=true)](https://ci.appveyor.com/project/foxbot/discord-net/branch/master) +[![NuGet Pre Release](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![AppVeyor](https://img.shields.io/appveyor/ci/foxbot/discord-net.svg?maxAge=2592000?style=plastic)](https://ci.appveyor.com/project/foxbot/discord-net/) [![Discord](https://discordapp.com/api/servers/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW) An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). From c1a076127973cb59bbdf1db8a53172330897bdcf Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 09:56:40 -0300 Subject: [PATCH 137/160] Started porting audio code from 0.9 --- src/Discord.Net.Audio/AudioClient.cs | 119 ++++++++++++++++++ src/Discord.Net.Audio/AudioMode.cs | 9 ++ src/Discord.Net.Audio/Discord.Net.Audio.xproj | 3 +- src/Discord.Net.Audio/LibSodium.cs | 23 ++++ src/Discord.Net.Audio/Opus/Ctl.cs | 10 ++ src/Discord.Net.Audio/Opus/OpusApplication.cs | 9 ++ src/Discord.Net.Audio/Opus/OpusConverter.cs | 41 ++++++ src/Discord.Net.Audio/Opus/OpusDecoder.cs | 48 +++++++ src/Discord.Net.Audio/Opus/OpusEncoder.cs | 100 +++++++++++++++ src/Discord.Net.Audio/Opus/OpusError.cs | 14 +++ src/Discord.Net.Audio/project.json | 5 +- 11 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 src/Discord.Net.Audio/AudioClient.cs create mode 100644 src/Discord.Net.Audio/AudioMode.cs create mode 100644 src/Discord.Net.Audio/LibSodium.cs create mode 100644 src/Discord.Net.Audio/Opus/Ctl.cs create mode 100644 src/Discord.Net.Audio/Opus/OpusApplication.cs create mode 100644 src/Discord.Net.Audio/Opus/OpusConverter.cs create mode 100644 src/Discord.Net.Audio/Opus/OpusDecoder.cs create mode 100644 src/Discord.Net.Audio/Opus/OpusEncoder.cs create mode 100644 src/Discord.Net.Audio/Opus/OpusError.cs diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs new file mode 100644 index 000000000..65173db55 --- /dev/null +++ b/src/Discord.Net.Audio/AudioClient.cs @@ -0,0 +1,119 @@ +using Discord.API; +using Discord.API.Voice; +using Discord.Net.Converters; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public class AudioClient + { + public const int MaxBitrate = 128; + + private const string Mode = "xsalsa20_poly1305"; + + private readonly JsonSerializer _serializer; + private readonly IWebSocketClient _gatewayClient; + private readonly SemaphoreSlim _connectionLock; + private CancellationTokenSource _connectCancelToken; + + public ConnectionState ConnectionState { get; private set; } + + internal AudioClient(WebSocketProvider provider, JsonSerializer serializer = null) + { + _connectionLock = new SemaphoreSlim(1, 1); + + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + + public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) + { + byte[] bytes = null; + payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + //TODO: Send + return Task.CompletedTask; + } + + //Gateway + public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) + { + await SendAsync(VoiceOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); + } + + + public async Task ConnectAsync(string url) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync(string url) + { + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync() + { + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Helpers + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + private string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + private T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + } +} diff --git a/src/Discord.Net.Audio/AudioMode.cs b/src/Discord.Net.Audio/AudioMode.cs new file mode 100644 index 000000000..b9acdbf89 --- /dev/null +++ b/src/Discord.Net.Audio/AudioMode.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio +{ + public enum AudioMode : byte + { + Outgoing = 1, + Incoming = 2, + Both = Outgoing | Incoming + } +} diff --git a/src/Discord.Net.Audio/Discord.Net.Audio.xproj b/src/Discord.Net.Audio/Discord.Net.Audio.xproj index 9f9783c6e..7434ce9ff 100644 --- a/src/Discord.Net.Audio/Discord.Net.Audio.xproj +++ b/src/Discord.Net.Audio/Discord.Net.Audio.xproj @@ -7,11 +7,10 @@ ddfcc44f-934e-478a-978c-69cdda2a1c5b - Discord.Net.Audio + Discord.Audio .\obj .\bin\ - 2.0 diff --git a/src/Discord.Net.Audio/LibSodium.cs b/src/Discord.Net.Audio/LibSodium.cs new file mode 100644 index 000000000..3b4129165 --- /dev/null +++ b/src/Discord.Net.Audio/LibSodium.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace Discord.Net.Audio +{ + public unsafe static class LibSodium + { + [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] + private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); + [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] + private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); + + public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + { + fixed (byte* outPtr = output) + return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); + } + public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) + { + fixed (byte* inPtr = input) + return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); + } + } +} diff --git a/src/Discord.Net.Audio/Opus/Ctl.cs b/src/Discord.Net.Audio/Opus/Ctl.cs new file mode 100644 index 000000000..5023782da --- /dev/null +++ b/src/Discord.Net.Audio/Opus/Ctl.cs @@ -0,0 +1,10 @@ +namespace Discord.Audio.Opus +{ + internal enum Ctl : int + { + SetBitrateRequest = 4002, + GetBitrateRequest = 4003, + SetInbandFECRequest = 4012, + GetInbandFECRequest = 4013 + } +} diff --git a/src/Discord.Net.Audio/Opus/OpusApplication.cs b/src/Discord.Net.Audio/Opus/OpusApplication.cs new file mode 100644 index 000000000..cbaa894a5 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/OpusApplication.cs @@ -0,0 +1,9 @@ +namespace Discord.Audio.Opus +{ + internal enum OpusApplication : int + { + Voice = 2048, + MusicOrMixed = 2049, + LowLatency = 2051 + } +} diff --git a/src/Discord.Net.Audio/Opus/OpusConverter.cs b/src/Discord.Net.Audio/Opus/OpusConverter.cs new file mode 100644 index 000000000..f430d07f7 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/OpusConverter.cs @@ -0,0 +1,41 @@ +using System; + +namespace Discord.Audio.Opus +{ + internal abstract class OpusConverter : IDisposable + { + protected IntPtr _ptr; + + /// Gets the bit rate of this converter. + public const int BitsPerSample = 16; + /// Gets the input sampling rate of this converter. + public int SamplingRate { get; } + + protected OpusConverter(int samplingRate) + { + if (samplingRate != 8000 && samplingRate != 12000 && + samplingRate != 16000 && samplingRate != 24000 && + samplingRate != 48000) + throw new ArgumentOutOfRangeException(nameof(samplingRate)); + + SamplingRate = samplingRate; + } + + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + disposedValue = true; + } + ~OpusConverter() + { + Dispose(false); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Discord.Net.Audio/Opus/OpusDecoder.cs b/src/Discord.Net.Audio/Opus/OpusDecoder.cs new file mode 100644 index 000000000..2df7c2414 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/OpusDecoder.cs @@ -0,0 +1,48 @@ +using System; +using System.Runtime.InteropServices; + +namespace Discord.Audio.Opus +{ + internal unsafe class OpusDecoder : OpusConverter + { + [DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error); + [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] + private static extern void DestroyDecoder(IntPtr decoder); + [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] + private static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); + + public OpusDecoder(int samplingRate) + : base(samplingRate) + { + OpusError error; + _ptr = CreateDecoder(samplingRate, 2, out error); + if (error != OpusError.OK) + throw new InvalidOperationException($"Error occured while creating decoder: {error}"); + } + + /// Produces PCM samples from Opus-encoded audio. + /// PCM samples to decode. + /// Offset of the frame in input. + /// Buffer to store the decoded frame. + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output) + { + int result = 0; + fixed (byte* inPtr = input) + result = Decode(_ptr, inPtr + inputOffset, inputCount, output, inputCount, 0); + + if (result < 0) + throw new Exception(((OpusError)result).ToString()); + return result; + } + + protected override void Dispose(bool disposing) + { + if (_ptr != IntPtr.Zero) + { + DestroyDecoder(_ptr); + _ptr = IntPtr.Zero; + } + } + } +} diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs new file mode 100644 index 000000000..92ac33317 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/OpusEncoder.cs @@ -0,0 +1,100 @@ +using System; +using System.Runtime.InteropServices; + +namespace Discord.Audio.Opus +{ + internal unsafe class OpusEncoder : OpusConverter + { + [DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error); + [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] + private static extern void DestroyEncoder(IntPtr encoder); + [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] + private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); + [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] + private static extern int EncoderCtl(IntPtr st, Ctl request, int value); + + /// Gets the bit rate in kbit/s. + public int? BitRate { get; } + /// Gets the coding mode of the encoder. + public OpusApplication Application { get; } + /// Gets the number of channels of this converter. + public int InputChannels { get; } + /// Gets the milliseconds per frame. + public int FrameMilliseconds { get; } + + /// Gets the bytes per sample. + public int SampleSize => (BitsPerSample / 8) * InputChannels; + /// Gets the number of samples per frame. + public int SamplesPerFrame => SamplingRate / 1000 * FrameMilliseconds; + /// Gets the bytes per frame. + public int FrameSize => SamplesPerFrame * SampleSize; + + public OpusEncoder(int samplingRate, int channels, int frameMillis, + int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed) + : base(samplingRate) + { + if (channels != 1 && channels != 2) + throw new ArgumentOutOfRangeException(nameof(channels)); + if (bitrate != null && (bitrate < 1 || bitrate > AudioClient.MaxBitrate)) + throw new ArgumentOutOfRangeException(nameof(bitrate)); + + OpusError error; + _ptr = CreateEncoder(samplingRate, channels, (int)application, out error); + if (error != OpusError.OK) + throw new InvalidOperationException($"Error occured while creating encoder: {error}"); + + + BitRate = bitrate; + Application = application; + InputChannels = channels; + FrameMilliseconds = frameMillis; + + SetForwardErrorCorrection(true); + if (bitrate != null) + SetBitrate(bitrate.Value); + } + + + /// Produces Opus encoded audio from PCM samples. + /// PCM samples to encode. + /// Offset of the frame in pcmSamples. + /// Buffer to store the encoded frame. + /// Length of the frame contained in outputBuffer. + public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) + { + int result = 0; + fixed (byte* inPtr = input) + result = Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); + + if (result < 0) + throw new Exception(((OpusError)result).ToString()); + return result; + } + + /// Gets or sets whether Forward Error Correction is enabled. + public void SetForwardErrorCorrection(bool value) + { + var result = EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0); + if (result < 0) + throw new Exception(((OpusError)result).ToString()); + } + + /// Gets or sets whether Forward Error Correction is enabled. + public void SetBitrate(int value) + { + var result = EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000); + if (result < 0) + throw new Exception(((OpusError)result).ToString()); + } + + protected override void Dispose(bool disposing) + { + if (_ptr != IntPtr.Zero) + { + DestroyEncoder(_ptr); + _ptr = IntPtr.Zero; + } + } + } +} diff --git a/src/Discord.Net.Audio/Opus/OpusError.cs b/src/Discord.Net.Audio/Opus/OpusError.cs new file mode 100644 index 000000000..5bfb92d98 --- /dev/null +++ b/src/Discord.Net.Audio/Opus/OpusError.cs @@ -0,0 +1,14 @@ +namespace Discord.Audio.Opus +{ + internal enum OpusError : int + { + OK = 0, + BadArg = -1, + BufferToSmall = -2, + InternalError = -3, + InvalidPacket = -4, + Unimplemented = -5, + InvalidState = -6, + AllocFail = -7 + } +} diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json index 64992a775..3d9ae74d2 100644 --- a/src/Discord.Net.Audio/project.json +++ b/src/Discord.Net.Audio/project.json @@ -1,4 +1,4 @@ -{ +{ "version": "1.0.0-dev", "description": "A Discord.Net extension adding audio support.", "authors": [ "RogueException" ], @@ -19,7 +19,8 @@ }, "dependencies": { - "Discord.Net": "1.0.0-dev" + "Discord.Net": "1.0.0-dev", + "System.Runtime.InteropServices": "4.1.0-rc2-24027" }, "frameworks": { From ec2a85745563a9af270df73514510adeca9af1f1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 22:23:32 -0300 Subject: [PATCH 138/160] Did more groundwork for AudioClient, added ILogManager --- src/Discord.Net.Audio/AudioAPIClient.cs | 176 +++++++++++ src/Discord.Net.Audio/AudioClient.cs | 287 ++++++++++++++---- src/Discord.Net.Audio/AudioConfig.cs | 17 ++ src/Discord.Net.Audio/Logger.cs | 6 + src/Discord.Net.Audio/Opus/OpusEncoder.cs | 2 +- src/Discord.Net.Audio/Utilities/AsyncEvent.cs | 74 +++++ src/Discord.Net/API/Gateway/GatewayOpCode.cs | 10 +- src/Discord.Net/API/Voice/IdentifyParams.cs | 16 + src/Discord.Net/API/Voice/VoiceOpCode.cs | 4 +- src/Discord.Net/DiscordClient.cs | 2 +- src/Discord.Net/DiscordSocketClient.cs | 15 +- src/Discord.Net/Logging/ILogManager.cs | 36 +++ src/Discord.Net/Logging/LogManager.cs | 4 +- 13 files changed, 581 insertions(+), 68 deletions(-) create mode 100644 src/Discord.Net.Audio/AudioAPIClient.cs create mode 100644 src/Discord.Net.Audio/AudioConfig.cs create mode 100644 src/Discord.Net.Audio/Logger.cs create mode 100644 src/Discord.Net.Audio/Utilities/AsyncEvent.cs create mode 100644 src/Discord.Net/API/Voice/IdentifyParams.cs create mode 100644 src/Discord.Net/Logging/ILogManager.cs diff --git a/src/Discord.Net.Audio/AudioAPIClient.cs b/src/Discord.Net.Audio/AudioAPIClient.cs new file mode 100644 index 000000000..db3418610 --- /dev/null +++ b/src/Discord.Net.Audio/AudioAPIClient.cs @@ -0,0 +1,176 @@ +using Discord.API; +using Discord.API.Voice; +using Discord.Net.Converters; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public class AudioAPIClient + { + public const int MaxBitrate = 128; + private const string Mode = "xsalsa20_poly1305"; + + public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } + private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + + public event Func ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } + private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + + private readonly ulong _userId; + private readonly string _token; + private readonly JsonSerializer _serializer; + private readonly IWebSocketClient _gatewayClient; + private readonly SemaphoreSlim _connectionLock; + private CancellationTokenSource _connectCancelToken; + + public ulong GuildId { get; } + public string SessionId { get; } + public ConnectionState ConnectionState { get; private set; } + + internal AudioAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + { + GuildId = guildId; + _userId = userId; + SessionId = sessionId; + _token = token; + _connectionLock = new SemaphoreSlim(1, 1); + + _gatewayClient = webSocketProvider(); + //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) + _gatewayClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + { + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + } + } + }; + _gatewayClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(text); + await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); + }; + _gatewayClient.Closed += async ex => + { + await DisconnectAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); + }; + + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + } + + public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) + { + byte[] bytes = null; + payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; + if (payload != null) + bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); + //TODO: Send + return Task.CompletedTask; + } + + //WebSocket + public async Task SendHeartbeatAsync(RequestOptions options = null) + { + await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); + } + public async Task SendIdentityAsync(ulong guildId, ulong userId, string sessionId, string token) + { + await SendAsync(VoiceOpCode.Identify, new IdentifyParams + { + GuildId = guildId, + UserId = userId, + SessionId = sessionId, + Token = token + }); + } + + public async Task ConnectAsync(string url) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternalAsync(string url) + { + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + + await SendIdentityAsync(GuildId, _userId, SessionId, _token).ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternalAsync().ConfigureAwait(false); + throw; + } + } + + public async Task DisconnectAsync() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternalAsync() + { + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; + } + + //Helpers + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + private string SerializeJson(object value) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value); + return sb.ToString(); + } + private T DeserializeJson(Stream jsonStream) + { + using (TextReader text = new StreamReader(jsonStream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); + } + } +} diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs index 65173db55..7f9298ed0 100644 --- a/src/Discord.Net.Audio/AudioClient.cs +++ b/src/Discord.Net.Audio/AudioClient.cs @@ -1,13 +1,9 @@ -using Discord.API; -using Discord.API.Voice; +using Discord.API.Voice; +using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -15,60 +11,115 @@ namespace Discord.Audio { public class AudioClient { - public const int MaxBitrate = 128; - - private const string Mode = "xsalsa20_poly1305"; + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + private readonly ILogger _webSocketLogger; +#if BENCHMARK + private readonly ILogger _benchmarkLogger; +#endif private readonly JsonSerializer _serializer; - private readonly IWebSocketClient _gatewayClient; - private readonly SemaphoreSlim _connectionLock; - private CancellationTokenSource _connectCancelToken; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + internal readonly SemaphoreSlim _connectionLock; + + private TaskCompletionSource _connectTask; + private CancellationTokenSource _cancelToken; + private Task _heartbeatTask, _reconnectTask; + private long _heartbeatTime; + private bool _isReconnecting; + private string _url; + public AudioAPIClient ApiClient { get; private set; } + /// Gets the current connection state of this client. public ConnectionState ConnectionState { get; private set; } + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + public int Latency { get; private set; } - internal AudioClient(WebSocketProvider provider, JsonSerializer serializer = null) + /// Creates a new REST/WebSocket discord client. + internal AudioClient(ulong guildId, ulong userId, string sessionId, string token, AudioConfig config, ILogManager logManager) { - _connectionLock = new SemaphoreSlim(1, 1); + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - } + _webSocketLogger = logManager.CreateLogger("AudioWS"); +#if BENCHMARK + _benchmarkLogger = logManager.CreateLogger("Benchmark"); +#endif - public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) - { - byte[] bytes = null; - payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; - if (payload != null) - bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - //TODO: Send - return Task.CompletedTask; - } + _connectionLock = new SemaphoreSlim(1, 1); - //Gateway - public async Task SendHeartbeatAsync(int lastSeq, RequestOptions options = null) - { - await SendAsync(VoiceOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); - } + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer.Error += (s, e) => + { + _webSocketLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + e.ErrorContext.Handled = true; + }; + + var webSocketProvider = config.WebSocketProvider; //TODO: Clean this check + ApiClient = new AudioAPIClient(guildId, userId, sessionId, token, config.WebSocketProvider); + ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); + ApiClient.ReceivedEvent += ProcessMessageAsync; + ApiClient.Disconnected += async ex => + { + if (ex != null) + { + await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + } + else + await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); + }; + } + /// public async Task ConnectAsync(string url) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await ConnectInternalAsync(url).ConfigureAwait(false); } finally { _connectionLock.Release(); } } private async Task ConnectInternalAsync(string url) { + var state = ConnectionState; + if (state == ConnectionState.Connecting || state == ConnectionState.Connected) + await DisconnectInternalAsync().ConfigureAwait(false); + ConnectionState = ConnectionState.Connecting; + await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); try { - _connectCancelToken = new CancellationTokenSource(); - _gatewayClient.SetCancelToken(_connectCancelToken.Token); - await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); + _url = url; + _connectTask = new TaskCompletionSource(); + _cancelToken = new CancellationTokenSource(); + await ApiClient.ConnectAsync(url).ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); + + await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; + await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); } catch (Exception) { @@ -76,12 +127,13 @@ namespace Discord.Audio throw; } } - + /// public async Task DisconnectAsync() { await _connectionLock.WaitAsync().ConfigureAwait(false); try { + _isReconnecting = false; await DisconnectInternalAsync().ConfigureAwait(false); } finally { _connectionLock.Release(); } @@ -90,30 +142,163 @@ namespace Discord.Audio { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - - try { _connectCancelToken?.Cancel(false); } - catch { } + await _webSocketLogger.InfoAsync("Disconnecting").ConfigureAwait(false); - await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + //Signal tasks to complete + try { _cancelToken.Cancel(); } catch { } + + //Disconnect from server + await ApiClient.DisconnectAsync().ConfigureAwait(false); + + //Wait for tasks to complete + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; ConnectionState = ConnectionState.Disconnected; + await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); + + await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); + } + + private async Task StartReconnectAsync() + { + //TODO: Is this thread-safe? + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + if (_reconnectTask != null) return; + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); + } + finally { _connectionLock.Release(); } + } + private async Task ReconnectInternalAsync() + { + try + { + int nextReconnectDelay = 1000; + while (_isReconnecting) + { + try + { + await Task.Delay(nextReconnectDelay).ConfigureAwait(false); + nextReconnectDelay *= 2; + if (nextReconnectDelay > 30000) + nextReconnectDelay = 30000; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(_url).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + return; + } + catch (Exception ex) + { + await _webSocketLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); + } + } + } + finally + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + _reconnectTask = null; + } + finally { _connectionLock.Release(); } + } } - //Helpers - private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - private string SerializeJson(object value) + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { - var sb = new StringBuilder(256); - using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) - using (JsonWriter writer = new JsonTextWriter(text)) - _serializer.Serialize(writer, value); - return sb.ToString(); +#if BENCHMARK + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { +#endif + try + { + switch (opCode) + { + /*case VoiceOpCode.Ready: + { + await _webSocketLogger.DebugAsync("Received Ready").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + _heartbeatTime = 0; + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); + } + break;*/ + case VoiceOpCode.HeartbeatAck: + { + await _webSocketLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + + var heartbeatTime = _heartbeatTime; + if (heartbeatTime != 0) + { + int latency = (int)(Environment.TickCount - _heartbeatTime); + _heartbeatTime = 0; + await _webSocketLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + + int before = Latency; + Latency = latency; + + await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + } + } + break; + default: + await _webSocketLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + return; + } + } + catch (Exception ex) + { + await _webSocketLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + return; + } +#if BENCHMARK + } + finally + { + stopwatch.Stop(); + double millis = Math.Round(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + await _benchmarkLogger.DebugAsync($"{millis} ms").ConfigureAwait(false); + } +#endif } - private T DeserializeJson(Stream jsonStream) + + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { - using (TextReader text = new StreamReader(jsonStream)) - using (JsonReader reader = new JsonTextReader(text)) - return _serializer.Deserialize(reader); + //Clean this up when Discord's session patch is live + try + { + while (!cancelToken.IsCancellationRequested) + { + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + + if (_heartbeatTime != 0) //Server never responded to our last heartbeat + { + if (ConnectionState == ConnectionState.Connected) + { + await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + return; + } + } + else + _heartbeatTime = Environment.TickCount; + await ApiClient.SendHeartbeatAsync().ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } } } } diff --git a/src/Discord.Net.Audio/AudioConfig.cs b/src/Discord.Net.Audio/AudioConfig.cs new file mode 100644 index 000000000..43db99020 --- /dev/null +++ b/src/Discord.Net.Audio/AudioConfig.cs @@ -0,0 +1,17 @@ +using Discord.Net.WebSockets; + +namespace Discord.Audio +{ + public class AudioConfig + { + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. + public int ConnectionTimeout { get; set; } = 30000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; + + /// Gets or sets the provider used to generate new websocket connections. + public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + } +} diff --git a/src/Discord.Net.Audio/Logger.cs b/src/Discord.Net.Audio/Logger.cs new file mode 100644 index 000000000..cdb3b5fa6 --- /dev/null +++ b/src/Discord.Net.Audio/Logger.cs @@ -0,0 +1,6 @@ +namespace Discord.Audio +{ + internal class Logger + { + } +} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs index 92ac33317..2e1d4d861 100644 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusEncoder.cs @@ -36,7 +36,7 @@ namespace Discord.Audio.Opus { if (channels != 1 && channels != 2) throw new ArgumentOutOfRangeException(nameof(channels)); - if (bitrate != null && (bitrate < 1 || bitrate > AudioClient.MaxBitrate)) + if (bitrate != null && (bitrate < 1 || bitrate > AudioAPIClient.MaxBitrate)) throw new ArgumentOutOfRangeException(nameof(bitrate)); OpusError error; diff --git a/src/Discord.Net.Audio/Utilities/AsyncEvent.cs b/src/Discord.Net.Audio/Utilities/AsyncEvent.cs new file mode 100644 index 000000000..e94b1d892 --- /dev/null +++ b/src/Discord.Net.Audio/Utilities/AsyncEvent.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Discord +{ + public class AsyncEvent + { + private readonly object _subLock = new object(); + internal ImmutableArray _subscriptions; + + public IReadOnlyList Subscriptions => _subscriptions; + + public AsyncEvent() + { + _subscriptions = ImmutableArray.Create(); + } + + public void Add(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Add(subscriber); + } + public void Remove(T subscriber) + { + lock (_subLock) + _subscriptions = _subscriptions.Remove(subscriber); + } + } + + public static class EventExtensions + { + public static async Task InvokeAsync(this AsyncEvent> eventHandler) + { + var subscribers = eventHandler.Subscriptions; + if (subscribers.Count > 0) + { + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); + } + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + var subscribers = eventHandler.Subscriptions; + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/API/Gateway/GatewayOpCode.cs b/src/Discord.Net/API/Gateway/GatewayOpCode.cs index 8b983383f..dc69d073c 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCode.cs @@ -2,7 +2,7 @@ { public enum GatewayOpCode : byte { - /// S→C - Used to send most events. + /// C←S - Used to send most events. Dispatch = 0, /// C↔S - Used to keep the connection alive and measure latency. Heartbeat = 1, @@ -16,15 +16,15 @@ VoiceServerPing = 5, /// C→S - Used to resume a connection after a redirect occurs. Resume = 6, - /// S→C - Used to notify a client that they must reconnect to another gateway. + /// 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, - /// S→C - Used to notify the client that their session has expired and cannot be resumed. + /// C←S - Used to notify the client that their session has expired and cannot be resumed. InvalidSession = 9, - /// S→C - Used to provide information to the client immediately on connection. + /// C←S - Used to provide information to the client immediately on connection. Hello = 10, - /// S→C - Used to reply to a client's heartbeat. + /// C←S - Used to reply to a client's heartbeat. HeartbeatAck = 11 } } diff --git a/src/Discord.Net/API/Voice/IdentifyParams.cs b/src/Discord.Net/API/Voice/IdentifyParams.cs new file mode 100644 index 000000000..39889bfad --- /dev/null +++ b/src/Discord.Net/API/Voice/IdentifyParams.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class IdentifyParams + { + [JsonProperty("server_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/VoiceOpCode.cs b/src/Discord.Net/API/Voice/VoiceOpCode.cs index b1526b463..b571f2d96 100644 --- a/src/Discord.Net/API/Voice/VoiceOpCode.cs +++ b/src/Discord.Net/API/Voice/VoiceOpCode.cs @@ -8,8 +8,10 @@ SelectProtocol = 1, /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. Ready = 2, - /// C↔S - Used to keep the connection alive and measure latency. + /// C→S - Used to keep the connection alive and measure latency. Heartbeat = 3, + /// C←S - Used to reply to a client's heartbeat. + HeartbeatAck = 3, /// C←S - Used to provide an encryption key to the client. SessionDescription = 4, /// C↔S - Used to inform that a certain user is speaking. diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 3e87dee1e..49681702b 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -24,7 +24,7 @@ namespace Discord public event Func LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } } private readonly AsyncEvent> _loggedOutEvent = new AsyncEvent>(); - internal readonly Logger _discordLogger, _restLogger, _queueLogger; + internal readonly ILogger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; internal readonly LogManager _log; internal readonly RequestQueue _requestQueue; diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index f27a26657..01a9ae88c 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -2,7 +2,6 @@ using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -21,9 +20,9 @@ namespace Discord public partial class DiscordSocketClient : DiscordClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; - private readonly Logger _gatewayLogger; + private readonly ILogger _gatewayLogger; #if BENCHMARK - private readonly Logger _benchmarkLogger; + private readonly ILogger _benchmarkLogger; #endif private readonly JsonSerializer _serializer; private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; @@ -150,6 +149,11 @@ namespace Discord await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); + if (_sessionId != null) + await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); + else + await ApiClient.SendIdentifyAsync().ConfigureAwait(false); + await _connectTask.Task.ConfigureAwait(false); ConnectionState = ConnectionState.Connected; @@ -205,6 +209,7 @@ namespace Discord await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); } + private async Task StartReconnectAsync() { //TODO: Is this thread-safe? @@ -416,10 +421,6 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (_sessionId != null) - await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); - else - await ApiClient.SendIdentifyAsync().ConfigureAwait(false); _heartbeatTime = 0; _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); } diff --git a/src/Discord.Net/Logging/ILogManager.cs b/src/Discord.Net/Logging/ILogManager.cs new file mode 100644 index 000000000..b244419b9 --- /dev/null +++ b/src/Discord.Net/Logging/ILogManager.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Logging +{ + public interface ILogManager + { + LogSeverity Level { get; } + + Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null); + Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null); + Task LogAsync(LogSeverity severity, string source, Exception ex); + + Task ErrorAsync(string source, string message, Exception ex = null); + Task ErrorAsync(string source, FormattableString message, Exception ex = null); + Task ErrorAsync(string source, Exception ex); + + Task WarningAsync(string source, string message, Exception ex = null); + Task WarningAsync(string source, FormattableString message, Exception ex = null); + Task WarningAsync(string source, Exception ex); + + Task InfoAsync(string source, string message, Exception ex = null); + Task InfoAsync(string source, FormattableString message, Exception ex = null); + Task InfoAsync(string source, Exception ex); + + Task VerboseAsync(string source, string message, Exception ex = null); + Task VerboseAsync(string source, FormattableString message, Exception ex = null); + Task VerboseAsync(string source, Exception ex); + + Task DebugAsync(string source, string message, Exception ex = null); + Task DebugAsync(string source, FormattableString message, Exception ex = null); + Task DebugAsync(string source, Exception ex); + + ILogger CreateLogger(string name); + } +} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index d428ae59f..e37b2bce6 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Logging { - internal class LogManager : ILogger + internal class LogManager : ILogManager, ILogger { public LogSeverity Level { get; } @@ -111,6 +111,6 @@ namespace Discord.Logging Task ILogger.DebugAsync(Exception ex) => LogAsync(LogSeverity.Debug, "Discord", ex); - public Logger CreateLogger(string name) => new Logger(this, name); + public ILogger CreateLogger(string name) => new Logger(this, name); } } From 5544d0759b76c1f3a37db062505f176ca4f1f3e7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 27 Jun 2016 22:23:45 -0300 Subject: [PATCH 139/160] Updated to .Net Core 1.0 RTM --- README.md | 8 +++----- global.json | 5 +---- src/Discord.Net.Audio/project.json | 2 +- src/Discord.Net/project.json | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 247a05999..4eb5dd487 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,14 @@ Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.htm You can download Discord.Net and its extensions from NuGet: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) -- [Discord.Net.Modules](https://www.nuget.org/packages/Discord.Net.Modules/) - [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) ### Compiling In order to compile Discord.Net, you require the following: #### Visual Studio 2015 -- [VS2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) -- [.Net Core SDK + VS Plugin](https://www.microsoft.com/net/core#windows) -- NuGet 3.3+ (available through Visual Studio) +- [VS2015 Update 3](https://www.microsoft.com/net/core#windows) +- [.Net Core 1.0 VS Plugin](https://www.microsoft.com/net/core#windows) #### CLI -- [.Net Core SDK](https://www.microsoft.com/net/core#windows) +- [.Net Core 1.0 SDK](https://www.microsoft.com/net/core) diff --git a/global.json b/global.json index 7ee23dc6a..9a66d5edc 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,3 @@ { - "projects": [ "src", "test" ], - "sdk": { - "version": "1.0.0-preview1-002702" - } + "projects": [ "src", "test" ] } diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json index 3d9ae74d2..c7788f942 100644 --- a/src/Discord.Net.Audio/project.json +++ b/src/Discord.Net.Audio/project.json @@ -20,7 +20,7 @@ "dependencies": { "Discord.Net": "1.0.0-dev", - "System.Runtime.InteropServices": "4.1.0-rc2-24027" + "System.Runtime.InteropServices": "4.1.0" }, "frameworks": { diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 7f202690e..04969b27b 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -19,17 +19,17 @@ }, "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1-rc2-24027", + "Microsoft.Win32.Primitives": "4.0.1", "Newtonsoft.Json": "8.0.3", - "System.Collections.Concurrent": "4.0.12-rc2-24027", - "System.Collections.Immutable": "1.2.0-rc2-24027", - "System.IO.Compression": "4.1.0-rc2-24027", - "System.IO.FileSystem": "4.0.1-rc2-24027", - "System.Net.Http": "4.0.1-rc2-24027", - "System.Net.WebSockets.Client": "4.0.0-rc2-24027", - "System.Reflection.Extensions": "4.0.1-rc2-24027", - "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027", - "System.Text.RegularExpressions": "4.0.12-rc2-24027" + "System.Collections.Concurrent": "4.0.12", + "System.Collections.Immutable": "1.2.0", + "System.IO.Compression": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.Net.Http": "4.1.0", + "System.Net.WebSockets.Client": "4.0.0", + "System.Reflection.Extensions": "4.0.1", + "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Text.RegularExpressions": "4.1.0" }, "frameworks": { From d4c2a175161f6f7bf761b28079bee13c39ed4613 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 02:46:58 -0300 Subject: [PATCH 140/160] Moved IDMChannel/IGuildChannel's IUpdateable to IChannel --- src/Discord.Net/Entities/Channels/IChannel.cs | 2 +- src/Discord.Net/Entities/Channels/IDMChannel.cs | 2 +- src/Discord.Net/Entities/Channels/IGuildChannel.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs index b64e0f2c7..f27504e94 100644 --- a/src/Discord.Net/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IChannel : ISnowflakeEntity + public interface IChannel : ISnowflakeEntity, IUpdateable { /// Gets a collection of all users in this channel. Task> GetUsersAsync(); diff --git a/src/Discord.Net/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Entities/Channels/IDMChannel.cs index 2714c59f5..b6bbb39d6 100644 --- a/src/Discord.Net/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net/Entities/Channels/IDMChannel.cs @@ -2,7 +2,7 @@ namespace Discord { - public interface IDMChannel : IMessageChannel, IUpdateable + public interface IDMChannel : IMessageChannel { /// Gets the recipient of all messages in this channel. IUser Recipient { get; } diff --git a/src/Discord.Net/Entities/Channels/IGuildChannel.cs b/src/Discord.Net/Entities/Channels/IGuildChannel.cs index 60f52d2b5..50da5fa58 100644 --- a/src/Discord.Net/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/IGuildChannel.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IGuildChannel : IChannel, IDeletable, IUpdateable + public interface IGuildChannel : IChannel, IDeletable { /// Gets the name of this channel. string Name { get; } From 58d54a76b5f3d3a68ae5025296440a29fea9ce9d Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 06:20:16 -0300 Subject: [PATCH 141/160] Merged Discord.Net.Audio into the main project, starting adding IVoiceChannel.ConnectAsync() --- src/Discord.Net.Audio/AudioConfig.cs | 17 ---- src/Discord.Net.Audio/Discord.Net.Audio.xproj | 18 ---- src/Discord.Net.Audio/Logger.cs | 6 -- src/Discord.Net.Audio/Utilities/AsyncEvent.cs | 74 ----------------- src/Discord.Net.Audio/project.json | 35 -------- src/Discord.Net/API/DiscordAPIClient.cs | 11 +++ .../API/DiscordVoiceAPIClient.cs} | 4 +- .../API/Gateway/GuildEmojiUpdateEvent.cs | 12 +++ .../API/Gateway/RequestMembersParams.cs | 8 +- .../API/Gateway/UpdateVoiceParams.cs | 16 ---- .../API/Gateway/VoiceStateUpdateParams.cs | 21 +++++ .../Audio}/AudioClient.cs | 24 +++--- .../Audio}/AudioMode.cs | 6 +- src/Discord.Net/Audio/IAudioClient.cs | 20 +++++ .../Audio}/Opus/Ctl.cs | 0 .../Audio}/Opus/OpusApplication.cs | 0 .../Audio}/Opus/OpusConverter.cs | 0 .../Audio}/Opus/OpusDecoder.cs | 0 .../Audio}/Opus/OpusEncoder.cs | 2 +- .../Audio}/Opus/OpusError.cs | 0 .../Audio/Sodium/SecretBox.cs} | 4 +- src/Discord.Net/DiscordSocketClient.Events.cs | 1 + src/Discord.Net/DiscordSocketClient.cs | 82 ++++++++++++------- src/Discord.Net/DiscordSocketConfig.cs | 8 +- .../Entities/Channels/IVoiceChannel.cs | 3 + .../Entities/Channels/VoiceChannel.cs | 3 + src/Discord.Net/Entities/Guilds/Guild.cs | 2 + src/Discord.Net/Entities/Guilds/IGuild.cs | 3 + .../Entities/WebSocket/CachedGuild.cs | 15 +++- .../Entities/WebSocket/CachedVoiceChannel.cs | 17 +++- src/Discord.Net/project.json | 1 + 31 files changed, 191 insertions(+), 222 deletions(-) delete mode 100644 src/Discord.Net.Audio/AudioConfig.cs delete mode 100644 src/Discord.Net.Audio/Discord.Net.Audio.xproj delete mode 100644 src/Discord.Net.Audio/Logger.cs delete mode 100644 src/Discord.Net.Audio/Utilities/AsyncEvent.cs delete mode 100644 src/Discord.Net.Audio/project.json rename src/{Discord.Net.Audio/AudioAPIClient.cs => Discord.Net/API/DiscordVoiceAPIClient.cs} (97%) create mode 100644 src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs delete mode 100644 src/Discord.Net/API/Gateway/UpdateVoiceParams.cs create mode 100644 src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs rename src/{Discord.Net.Audio => Discord.Net/Audio}/AudioClient.cs (91%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/AudioMode.cs (62%) create mode 100644 src/Discord.Net/Audio/IAudioClient.cs rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/Ctl.cs (100%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/OpusApplication.cs (100%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/OpusConverter.cs (100%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/OpusDecoder.cs (100%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/OpusEncoder.cs (97%) rename src/{Discord.Net.Audio => Discord.Net/Audio}/Opus/OpusError.cs (100%) rename src/{Discord.Net.Audio/LibSodium.cs => Discord.Net/Audio/Sodium/SecretBox.cs} (93%) diff --git a/src/Discord.Net.Audio/AudioConfig.cs b/src/Discord.Net.Audio/AudioConfig.cs deleted file mode 100644 index 43db99020..000000000 --- a/src/Discord.Net.Audio/AudioConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord.Net.WebSockets; - -namespace Discord.Audio -{ - public class AudioConfig - { - /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. - public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get; set; } = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; - - /// Gets or sets the provider used to generate new websocket connections. - public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); - } -} diff --git a/src/Discord.Net.Audio/Discord.Net.Audio.xproj b/src/Discord.Net.Audio/Discord.Net.Audio.xproj deleted file mode 100644 index 7434ce9ff..000000000 --- a/src/Discord.Net.Audio/Discord.Net.Audio.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0.25123 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - ddfcc44f-934e-478a-978c-69cdda2a1c5b - Discord.Audio - .\obj - .\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.Audio/Logger.cs b/src/Discord.Net.Audio/Logger.cs deleted file mode 100644 index cdb3b5fa6..000000000 --- a/src/Discord.Net.Audio/Logger.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord.Audio -{ - internal class Logger - { - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Utilities/AsyncEvent.cs b/src/Discord.Net.Audio/Utilities/AsyncEvent.cs deleted file mode 100644 index e94b1d892..000000000 --- a/src/Discord.Net.Audio/Utilities/AsyncEvent.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; - -namespace Discord -{ - public class AsyncEvent - { - private readonly object _subLock = new object(); - internal ImmutableArray _subscriptions; - - public IReadOnlyList Subscriptions => _subscriptions; - - public AsyncEvent() - { - _subscriptions = ImmutableArray.Create(); - } - - public void Add(T subscriber) - { - lock (_subLock) - _subscriptions = _subscriptions.Add(subscriber); - } - public void Remove(T subscriber) - { - lock (_subLock) - _subscriptions = _subscriptions.Remove(subscriber); - } - } - - public static class EventExtensions - { - public static async Task InvokeAsync(this AsyncEvent> eventHandler) - { - var subscribers = eventHandler.Subscriptions; - if (subscribers.Count > 0) - { - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke().ConfigureAwait(false); - } - } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) - { - var subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg).ConfigureAwait(false); - } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2) - { - var subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2).ConfigureAwait(false); - } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3) - { - var subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3).ConfigureAwait(false); - } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - var subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); - } - public static async Task InvokeAsync(this AsyncEvent> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) - { - var subscribers = eventHandler.Subscriptions; - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); - } - } -} diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json deleted file mode 100644 index c7788f942..000000000 --- a/src/Discord.Net.Audio/project.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "version": "1.0.0-dev", - "description": "A Discord.Net extension adding audio support.", - "authors": [ "RogueException" ], - - "packOptions": { - "tags": [ "discord", "discordapp" ], - "licenseUrl": "http://opensource.org/licenses/MIT", - "projectUrl": "https://github.com/RogueException/Discord.Net", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - } - }, - - "buildOptions": { - "allowUnsafe": true, - "warningsAsErrors": false - }, - - "dependencies": { - "Discord.Net": "1.0.0-dev", - "System.Runtime.InteropServices": "4.1.0" - }, - - "frameworks": { - "netstandard1.3": { - "imports": [ - "dotnet5.4", - "dnxcore50", - "portable-net45+win8" - ] - } - } -} diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 2a0b4c02e..128c15474 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -398,6 +398,17 @@ namespace Discord.API { await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); } + public async Task SendVoiceStateUpdateAsync(ulong guildId, ulong? channelId, bool selfDeaf, bool selfMute, RequestOptions options = null) + { + var payload = new VoiceStateUpdateParams + { + GuildId = guildId, + ChannelId = channelId, + SelfDeaf = selfDeaf, + SelfMute = selfMute + }; + await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net.Audio/AudioAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs similarity index 97% rename from src/Discord.Net.Audio/AudioAPIClient.cs rename to src/Discord.Net/API/DiscordVoiceAPIClient.cs index db3418610..e64d74c10 100644 --- a/src/Discord.Net.Audio/AudioAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - public class AudioAPIClient + public class DiscordVoiceAPIClient { public const int MaxBitrate = 128; private const string Mode = "xsalsa20_poly1305"; @@ -40,7 +40,7 @@ namespace Discord.Audio public string SessionId { get; } public ConnectionState ConnectionState { get; private set; } - internal AudioAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + internal DiscordVoiceAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; _userId = userId; diff --git a/src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs new file mode 100644 index 000000000..13f083d40 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildEmojiUpdateEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildEmojiUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId; + [JsonProperty("emojis")] + public Emoji[] Emojis; + } +} diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs index f11be49b1..a0819c556 100644 --- a/src/Discord.Net/API/Gateway/RequestMembersParams.cs +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -1,15 +1,19 @@ using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Discord.API.Gateway { public class RequestMembersParams { - [JsonProperty("guild_id")] - public IEnumerable GuildIds { get; set; } [JsonProperty("query")] public string Query { get; set; } [JsonProperty("limit")] public int Limit { get; set; } + + [JsonProperty("guild_id")] + public IEnumerable GuildIds { get; set; } + [JsonIgnore] + public IEnumerable Guilds { set { GuildIds = value.Select(x => x.Id); } } } } diff --git a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs deleted file mode 100644 index d72d63548..000000000 --- a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Gateway -{ - public class UpdateVoiceParams - { - [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/VoiceStateUpdateParams.cs b/src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs new file mode 100644 index 000000000..6eb285cea --- /dev/null +++ b/src/Discord.Net/API/Gateway/VoiceStateUpdateParams.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class VoiceStateUpdateParams + { + [JsonProperty("self_mute")] + public bool SelfMute { get; set; } + [JsonProperty("self_deaf")] + public bool SelfDeaf { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonIgnore] + public IGuild Guild { set { GuildId = value.Id; } } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + [JsonIgnore] + public IChannel Channel { set { ChannelId = value?.Id; } } + } +} diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs similarity index 91% rename from src/Discord.Net.Audio/AudioClient.cs rename to src/Discord.Net/Audio/AudioClient.cs index 7f9298ed0..854c3219b 100644 --- a/src/Discord.Net.Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -1,15 +1,15 @@ using Discord.API.Voice; using Discord.Logging; using Discord.Net.Converters; +using Discord.Net.WebSockets; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { - public class AudioClient + internal class AudioClient : IAudioClient { public event Func Connected { @@ -30,12 +30,11 @@ namespace Discord.Audio } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - private readonly ILogger _webSocketLogger; + private readonly ILogger _webSocketLogger, _udpLogger; #if BENCHMARK private readonly ILogger _benchmarkLogger; #endif private readonly JsonSerializer _serializer; - private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; internal readonly SemaphoreSlim _connectionLock; private TaskCompletionSource _connectTask; @@ -45,20 +44,18 @@ namespace Discord.Audio private bool _isReconnecting; private string _url; - public AudioAPIClient ApiClient { get; private set; } - /// Gets the current connection state of this client. + private DiscordSocketClient Discord { get; } + public DiscordVoiceAPIClient ApiClient { get; private set; } public ConnectionState ConnectionState { get; private set; } - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } /// Creates a new REST/WebSocket discord client. - internal AudioClient(ulong guildId, ulong userId, string sessionId, string token, AudioConfig config, ILogManager logManager) + internal AudioClient(DiscordSocketClient discord, ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, ILogManager logManager) { - _connectionTimeout = config.ConnectionTimeout; - _reconnectDelay = config.ReconnectDelay; - _failedReconnectDelay = config.FailedReconnectDelay; + Discord = discord; - _webSocketLogger = logManager.CreateLogger("AudioWS"); + _webSocketLogger = logManager.CreateLogger("Audio"); + _udpLogger = logManager.CreateLogger("AudioUDP"); #if BENCHMARK _benchmarkLogger = logManager.CreateLogger("Benchmark"); #endif @@ -72,8 +69,7 @@ namespace Discord.Audio e.ErrorContext.Handled = true; }; - var webSocketProvider = config.WebSocketProvider; //TODO: Clean this check - ApiClient = new AudioAPIClient(guildId, userId, sessionId, token, config.WebSocketProvider); + ApiClient = new DiscordVoiceAPIClient(guildId, userId, sessionId, token, webSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; diff --git a/src/Discord.Net.Audio/AudioMode.cs b/src/Discord.Net/Audio/AudioMode.cs similarity index 62% rename from src/Discord.Net.Audio/AudioMode.cs rename to src/Discord.Net/Audio/AudioMode.cs index b9acdbf89..7cc5a08c1 100644 --- a/src/Discord.Net.Audio/AudioMode.cs +++ b/src/Discord.Net/Audio/AudioMode.cs @@ -1,7 +1,11 @@ -namespace Discord.Audio +using System; + +namespace Discord.Audio { + [Flags] public enum AudioMode : byte { + Disabled = 0, Outgoing = 1, Incoming = 2, Both = Outgoing | Incoming diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net/Audio/IAudioClient.cs new file mode 100644 index 000000000..5f59851ee --- /dev/null +++ b/src/Discord.Net/Audio/IAudioClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public interface IAudioClient + { + event Func Connected; + event Func Disconnected; + event Func LatencyUpdated; + + DiscordVoiceAPIClient ApiClient { get; } + /// Gets the current connection state of this client. + ConnectionState ConnectionState { get; } + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + int Latency { get; } + + Task DisconnectAsync(); + } +} diff --git a/src/Discord.Net.Audio/Opus/Ctl.cs b/src/Discord.Net/Audio/Opus/Ctl.cs similarity index 100% rename from src/Discord.Net.Audio/Opus/Ctl.cs rename to src/Discord.Net/Audio/Opus/Ctl.cs diff --git a/src/Discord.Net.Audio/Opus/OpusApplication.cs b/src/Discord.Net/Audio/Opus/OpusApplication.cs similarity index 100% rename from src/Discord.Net.Audio/Opus/OpusApplication.cs rename to src/Discord.Net/Audio/Opus/OpusApplication.cs diff --git a/src/Discord.Net.Audio/Opus/OpusConverter.cs b/src/Discord.Net/Audio/Opus/OpusConverter.cs similarity index 100% rename from src/Discord.Net.Audio/Opus/OpusConverter.cs rename to src/Discord.Net/Audio/Opus/OpusConverter.cs diff --git a/src/Discord.Net.Audio/Opus/OpusDecoder.cs b/src/Discord.Net/Audio/Opus/OpusDecoder.cs similarity index 100% rename from src/Discord.Net.Audio/Opus/OpusDecoder.cs rename to src/Discord.Net/Audio/Opus/OpusDecoder.cs diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net/Audio/Opus/OpusEncoder.cs similarity index 97% rename from src/Discord.Net.Audio/Opus/OpusEncoder.cs rename to src/Discord.Net/Audio/Opus/OpusEncoder.cs index 2e1d4d861..e17487f43 100644 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net/Audio/Opus/OpusEncoder.cs @@ -36,7 +36,7 @@ namespace Discord.Audio.Opus { if (channels != 1 && channels != 2) throw new ArgumentOutOfRangeException(nameof(channels)); - if (bitrate != null && (bitrate < 1 || bitrate > AudioAPIClient.MaxBitrate)) + if (bitrate != null && (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)) throw new ArgumentOutOfRangeException(nameof(bitrate)); OpusError error; diff --git a/src/Discord.Net.Audio/Opus/OpusError.cs b/src/Discord.Net/Audio/Opus/OpusError.cs similarity index 100% rename from src/Discord.Net.Audio/Opus/OpusError.cs rename to src/Discord.Net/Audio/Opus/OpusError.cs diff --git a/src/Discord.Net.Audio/LibSodium.cs b/src/Discord.Net/Audio/Sodium/SecretBox.cs similarity index 93% rename from src/Discord.Net.Audio/LibSodium.cs rename to src/Discord.Net/Audio/Sodium/SecretBox.cs index 3b4129165..727db2711 100644 --- a/src/Discord.Net.Audio/LibSodium.cs +++ b/src/Discord.Net/Audio/Sodium/SecretBox.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; -namespace Discord.Net.Audio +namespace Discord.Net.Audio.Sodium { - public unsafe static class LibSodium + public unsafe static class SecretBox { [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index b4d4db47d..545f3a4fb 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; namespace Discord { + //TODO: Add event docstrings public partial class DiscordSocketClient { //General diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 01a9ae88c..d09675e18 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1,7 +1,9 @@ using Discord.API.Gateway; +using Discord.Audio; using Discord.Extensions; using Discord.Logging; using Discord.Net.Converters; +using Discord.Net.WebSockets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -14,9 +16,6 @@ using System.Threading.Tasks; namespace Discord { - //TODO: Add event docstrings - //TODO: Add reconnect logic (+ensure the heartbeat task to shut down) - //TODO: Add resume logic public partial class DiscordSocketClient : DiscordClient, IDiscordClient { private readonly ConcurrentQueue _largeGuilds; @@ -25,9 +24,6 @@ namespace Discord private readonly ILogger _benchmarkLogger; #endif private readonly JsonSerializer _serializer; - private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; - private readonly int _largeThreshold; - private readonly int _totalShards; private string _sessionId; private int _lastSeq; @@ -46,9 +42,17 @@ namespace Discord public ConnectionState ConnectionState { get; private set; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } - + + //From DiscordConfig + internal int TotalShards { get; private set; } + internal int ConnectionTimeout { get; private set; } + internal int ReconnectDelay { get; private set; } + internal int FailedReconnectDelay { get; private set; } internal int MessageCacheSize { get; private set; } + internal int LargeThreshold { get; private set; } + internal AudioMode AudioMode { get; private set; } internal DataStore DataStore { get; private set; } + internal WebSocketProvider WebSocketProvider { get; private set; } internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; internal IReadOnlyCollection Guilds => DataStore.Guilds; @@ -62,15 +66,15 @@ namespace Discord : base(config) { ShardId = config.ShardId; - _totalShards = config.TotalShards; - - _connectionTimeout = config.ConnectionTimeout; - _reconnectDelay = config.ReconnectDelay; - _failedReconnectDelay = config.FailedReconnectDelay; - + TotalShards = config.TotalShards; + ConnectionTimeout = config.ConnectionTimeout; + ReconnectDelay = config.ReconnectDelay; + FailedReconnectDelay = config.FailedReconnectDelay; MessageCacheSize = config.MessageCacheSize; - _largeThreshold = config.LargeThreshold; - + LargeThreshold = config.LargeThreshold; + AudioMode = config.AudioMode; + WebSocketProvider = config.WebSocketProvider; + _gatewayLogger = _log.CreateLogger("Gateway"); #if BENCHMARK _benchmarkLogger = _log.CreateLogger("Benchmark"); @@ -471,7 +475,7 @@ namespace Discord case GatewayOpCode.Dispatch: switch (type) { - //Global + //Connection case "READY": { await _gatewayLogger.DebugAsync("Received Dispatch (READY)").ConfigureAwait(false); @@ -507,6 +511,11 @@ namespace Discord await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; + case "RESUMED": + await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); + + await _gatewayLogger.InfoAsync("Resume").ConfigureAwait(false); + return; //Guilds case "GUILD_CREATE": @@ -569,6 +578,28 @@ namespace Discord } } break; + case "GUILD_EMOJI_UPDATE": //TODO: Add + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJI_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(data, UpdateSource.WebSocket); + await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("GUILD_EMOJI_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + return; + case "GUILD_INTEGRATIONS_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + return; case "GUILD_DELETE": { var data = (payload as JToken).ToObject(_serializer); @@ -1099,26 +1130,17 @@ namespace Discord } } break; + case "VOICE_SERVER_UPDATE": + await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + return; - //Ignored + //Ignored (User only) case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); return; - case "MESSAGE_ACK": //TODO: Add (User only) + case "MESSAGE_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); return; - case "GUILD_EMOJIS_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); - return; - case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - return; - case "VOICE_SERVER_UPDATE": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - return; - case "RESUMED": //TODO: Add - await _gatewayLogger.DebugAsync("Ignored Dispatch (RESUMED)").ConfigureAwait(false); - return; //Others default: diff --git a/src/Discord.Net/DiscordSocketConfig.cs b/src/Discord.Net/DiscordSocketConfig.cs index a40fba0e7..cd54fcd8d 100644 --- a/src/Discord.Net/DiscordSocketConfig.cs +++ b/src/Discord.Net/DiscordSocketConfig.cs @@ -1,4 +1,5 @@ -using Discord.Net.WebSockets; +using Discord.Audio; +using Discord.Net.WebSockets; namespace Discord { @@ -28,7 +29,10 @@ namespace Discord /// Decreasing this may reduce CPU usage while increasing login time and network usage. /// public int LargeThreshold { get; set; } = 250; - + + /// Gets or sets the type of audio this DiscordClient supports. + public AudioMode AudioMode { get; set; } = AudioMode.Disabled; + /// Gets or sets the provider used to generate new websocket connections. public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); } diff --git a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs index fc90b2935..5f6e8c817 100644 --- a/src/Discord.Net/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Audio; using System; using System.Threading.Tasks; @@ -13,5 +14,7 @@ namespace Discord /// Modifies this voice channel. Task ModifyAsync(Action func); + /// Connects to this voice channel. + Task ConnectAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs index 4e2cb6e80..a7e32d5a4 100644 --- a/src/Discord.Net/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Channel; +using Discord.Audio; namespace Discord { @@ -49,6 +50,8 @@ namespace Discord throw new NotSupportedException(); } + public virtual Task ConnectAsync() { throw new NotSupportedException(); } + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; } } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index d4a47f302..31a0f5916 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -1,4 +1,5 @@ using Discord.API.Rest; +using Discord.Audio; using Discord.Extensions; using System; using System.Collections.Concurrent; @@ -311,6 +312,7 @@ namespace Discord IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + IAudioClient IGuild.AudioClient => null; IRole IGuild.GetRole(ulong id) => GetRole(id); } diff --git a/src/Discord.Net/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs index 7302e15f8..8979677ac 100644 --- a/src/Discord.Net/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Discord.API.Rest; +using Discord.Audio; namespace Discord { @@ -37,6 +38,8 @@ namespace Discord /// Gets the id of the region hosting this guild's voice channels. string VoiceRegionId { get; } + /// Gets the IAudioClient currently associated with this guild. + IAudioClient AudioClient { get; } /// Gets the built-in role containing all users in this guild. IRole EveryoneRole { get; } /// Gets a collection of all custom emojis for this guild. diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 4d0af0783..2f383b4bc 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -1,4 +1,5 @@ -using Discord.Extensions; +using Discord.Audio; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -6,6 +7,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using ChannelModel = Discord.API.Channel; +using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using ExtendedModel = Discord.API.Gateway.ExtendedGuild; using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; @@ -25,6 +27,7 @@ namespace Discord public bool Available { get; private set; } public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } + public IAudioClient AudioClient { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task DownloaderPromise => _downloaderPromise.Task; @@ -102,6 +105,16 @@ namespace Discord _voiceStates = voiceStates; } + public void Update(EmojiUpdateModel model, UpdateSource source) + { + if (source == UpdateSource.Rest && IsAttached) return; + + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + for (int i = 0; i < model.Emojis.Length; i++) + emojis.Add(new Emoji(model.Emojis[i])); + Emojis = emojis.ToImmutableArray(); + } + public override Task GetChannelAsync(ulong id) => Task.FromResult(GetChannel(id)); public override Task> GetChannelsAsync() => Task.FromResult>(Channels); public void AddChannel(ChannelModel model, DataStore dataStore, ConcurrentHashSet channels = null) diff --git a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs index 6b00d82b5..358464188 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using Discord.Audio; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -33,6 +35,19 @@ namespace Discord return null; } + public override async Task ConnectAsync() + { + var audioMode = Discord.AudioMode; + if (audioMode == AudioMode.Disabled) + throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); + + await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, Id, + (audioMode & AudioMode.Incoming) == 0, + (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); + return null; + //TODO: Block and return + } + public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; ICachedChannel ICachedChannel.Clone() => Clone(); diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 04969b27b..76133d6d7 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -28,6 +28,7 @@ "System.Net.Http": "4.1.0", "System.Net.WebSockets.Client": "4.0.0", "System.Reflection.Extensions": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", "System.Runtime.Serialization.Primitives": "4.1.1", "System.Text.RegularExpressions": "4.1.0" }, From a529b6bd079fba3ff451608177651c27560fbef1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 08:36:20 -0300 Subject: [PATCH 142/160] Added VOICE_SERVER_UPDATE handling --- src/Discord.Net/API/DiscordVoiceAPIClient.cs | 34 +++-- src/Discord.Net/Audio/AudioClient.cs | 124 ++++++------------ src/Discord.Net/Audio/IAudioClient.cs | 2 +- src/Discord.Net/DiscordClient.cs | 16 ++- src/Discord.Net/DiscordSocketClient.Events.cs | 4 +- src/Discord.Net/DiscordSocketClient.cs | 41 ++++-- .../Entities/WebSocket/CachedGuild.cs | 57 +++++++- src/Discord.Net/IDiscordClient.cs | 2 + 8 files changed, 159 insertions(+), 121 deletions(-) diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs index e64d74c10..ddb8f2b6b 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -28,24 +28,19 @@ namespace Discord.Audio private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - - private readonly ulong _userId; - private readonly string _token; + private readonly JsonSerializer _serializer; private readonly IWebSocketClient _gatewayClient; private readonly SemaphoreSlim _connectionLock; private CancellationTokenSource _connectCancelToken; + private bool _isDisposed; - public ulong GuildId { get; } - public string SessionId { get; } + public ulong GuildId { get; } public ConnectionState ConnectionState { get; private set; } - internal DiscordVoiceAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; - _userId = userId; - SessionId = sessionId; - _token = token; _connectionLock = new SemaphoreSlim(1, 1); _gatewayClient = webSocketProvider(); @@ -78,6 +73,19 @@ namespace Discord.Audio _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } + void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + (_gatewayClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) { @@ -105,16 +113,16 @@ namespace Discord.Audio }); } - public async Task ConnectAsync(string url) + public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync(url).ConfigureAwait(false); + await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(string url) + private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) { ConnectionState = ConnectionState.Connecting; try @@ -123,7 +131,7 @@ namespace Discord.Audio _gatewayClient.SetCancelToken(_connectCancelToken.Token); await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); - await SendIdentityAsync(GuildId, _userId, SessionId, _token).ConfigureAwait(false); + await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index 854c3219b..b59f3d267 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -1,7 +1,6 @@ using Discord.API.Voice; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; using System; using System.Threading; @@ -9,7 +8,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class AudioClient : IAudioClient + internal class AudioClient : IAudioClient, IDisposable { public event Func Connected { @@ -17,12 +16,12 @@ namespace Discord.Audio remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); public event Func LatencyUpdated { add { _latencyUpdatedEvent.Add(value); } @@ -34,28 +33,30 @@ namespace Discord.Audio #if BENCHMARK private readonly ILogger _benchmarkLogger; #endif - private readonly JsonSerializer _serializer; internal readonly SemaphoreSlim _connectionLock; + private readonly JsonSerializer _serializer; private TaskCompletionSource _connectTask; private CancellationTokenSource _cancelToken; - private Task _heartbeatTask, _reconnectTask; + private Task _heartbeatTask; private long _heartbeatTime; - private bool _isReconnecting; private string _url; + private bool _isDisposed; - private DiscordSocketClient Discord { get; } + public CachedGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public ConnectionState ConnectionState { get; private set; } public int Latency { get; private set; } + private DiscordSocketClient Discord => Guild.Discord; + /// Creates a new REST/WebSocket discord client. - internal AudioClient(DiscordSocketClient discord, ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, ILogManager logManager) + internal AudioClient(CachedGuild guild) { - Discord = discord; + Guild = guild; - _webSocketLogger = logManager.CreateLogger("Audio"); - _udpLogger = logManager.CreateLogger("AudioUDP"); + _webSocketLogger = Discord.LogManager.CreateLogger("Audio"); + _udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); #if BENCHMARK _benchmarkLogger = logManager.CreateLogger("Benchmark"); #endif @@ -69,38 +70,34 @@ namespace Discord.Audio e.ErrorContext.Handled = true; }; - ApiClient = new DiscordVoiceAPIClient(guildId, userId, sessionId, token, webSocketProvider); + ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.Disconnected += async ex => { if (ex != null) - { await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); - } else await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; } /// - public async Task ConnectAsync(string url) + public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _isReconnecting = false; - await ConnectInternalAsync(url).ConfigureAwait(false); + await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(string url) + private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) { var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); @@ -109,7 +106,7 @@ namespace Discord.Audio _url = url; _connectTask = new TaskCompletionSource(); _cancelToken = new CancellationTokenSource(); - await ApiClient.ConnectAsync(url).ConfigureAwait(false); + await ApiClient.ConnectAsync(url, userId, sessionId, token).ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -119,7 +116,7 @@ namespace Discord.Audio } catch (Exception) { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); throw; } } @@ -129,12 +126,20 @@ namespace Discord.Audio await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _isReconnecting = false; - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectAsync(Exception ex) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync() + private async Task DisconnectInternalAsync(Exception ex) { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; @@ -155,61 +160,7 @@ namespace Discord.Audio ConnectionState = ConnectionState.Disconnected; await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); - } - - private async Task StartReconnectAsync() - { - //TODO: Is this thread-safe? - if (_reconnectTask != null) return; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (_reconnectTask != null) return; - _isReconnecting = true; - _reconnectTask = ReconnectInternalAsync(); - } - finally { _connectionLock.Release(); } - } - private async Task ReconnectInternalAsync() - { - try - { - int nextReconnectDelay = 1000; - while (_isReconnecting) - { - try - { - await Task.Delay(nextReconnectDelay).ConfigureAwait(false); - nextReconnectDelay *= 2; - if (nextReconnectDelay > 30000) - nextReconnectDelay = 30000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync(_url).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - return; - } - catch (Exception ex) - { - await _webSocketLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); - } - } - } - finally - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - _isReconnecting = false; - _reconnectTask = null; - } - finally { _connectionLock.Release(); } - } + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) @@ -285,7 +236,7 @@ namespace Discord.Audio if (ConnectionState == ConnectionState.Connected) { await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); return; } } @@ -296,5 +247,14 @@ namespace Discord.Audio } catch (OperationCanceledException) { } } + + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + _isDisposed = true; + ApiClient.Dispose(); + } + /// + public void Dispose() => Dispose(true); } } diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net/Audio/IAudioClient.cs index 5f59851ee..40a75d4b5 100644 --- a/src/Discord.Net/Audio/IAudioClient.cs +++ b/src/Discord.Net/Audio/IAudioClient.cs @@ -6,7 +6,7 @@ namespace Discord.Audio public interface IAudioClient { event Func Connected; - event Func Disconnected; + event Func Disconnected; event Func LatencyUpdated; DiscordVoiceAPIClient ApiClient { get; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 49681702b..4cf53d2a3 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -26,13 +26,13 @@ namespace Discord internal readonly ILogger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; - internal readonly LogManager _log; internal readonly RequestQueue _requestQueue; internal bool _isDisposed; internal SelfUser _currentUser; + public API.DiscordApiClient ApiClient { get; } + internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } - public API.DiscordApiClient ApiClient { get; private set; } /// Creates a new REST-only discord client. public DiscordClient() @@ -40,11 +40,11 @@ namespace Discord /// Creates a new REST-only discord client. public DiscordClient(DiscordConfig config) { - _log = new LogManager(config.LogLevel); - _log.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _discordLogger = _log.CreateLogger("Discord"); - _restLogger = _log.CreateLogger("Rest"); - _queueLogger = _log.CreateLogger("Queue"); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _discordLogger = LogManager.CreateLogger("Discord"); + _restLogger = LogManager.CreateLogger("Rest"); + _queueLogger = LogManager.CreateLogger("Queue"); _connectionLock = new SemaphoreSlim(1, 1); @@ -267,6 +267,8 @@ namespace Discord public void Dispose() => Dispose(true); ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + ILogManager IDiscordClient.LogManager => LogManager; + Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } } diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index 545f3a4fb..092b19674 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -13,12 +13,12 @@ namespace Discord remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); public event Func Ready { add { _readyEvent.Add(value); } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index d09675e18..eb1096988 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -75,7 +75,7 @@ namespace Discord AudioMode = config.AudioMode; WebSocketProvider = config.WebSocketProvider; - _gatewayLogger = _log.CreateLogger("Gateway"); + _gatewayLogger = LogManager.CreateLogger("Gateway"); #if BENCHMARK _benchmarkLogger = _log.CreateLogger("Benchmark"); #endif @@ -94,7 +94,7 @@ namespace Discord if (ex != null) { await _gatewayLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(ex).ConfigureAwait(false); } else await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); @@ -112,7 +112,7 @@ namespace Discord protected override async Task OnLogoutAsync() { if (ConnectionState != ConnectionState.Disconnected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); _voiceRegions = ImmutableDictionary.Create(); } @@ -142,7 +142,7 @@ namespace Discord var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); @@ -165,7 +165,7 @@ namespace Discord } catch (Exception) { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); throw; } } @@ -176,11 +176,11 @@ namespace Discord try { _isReconnecting = false; - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync() + private async Task DisconnectInternalAsync(Exception ex) { ulong guildId; @@ -211,10 +211,10 @@ namespace Discord ConnectionState = ConnectionState.Disconnected; await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } - private async Task StartReconnectAsync() + private async Task StartReconnectAsync(Exception ex) { //TODO: Is this thread-safe? if (_reconnectTask != null) return; @@ -222,6 +222,7 @@ namespace Discord await _connectionLock.WaitAsync().ConfigureAwait(false); try { + await DisconnectInternalAsync(ex).ConfigureAwait(false); if (_reconnectTask != null) return; _isReconnecting = true; _reconnectTask = ReconnectInternalAsync(); @@ -469,7 +470,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(new Exception("Server requested a reconnect")).ConfigureAwait(false); } break; case GatewayOpCode.Dispatch: @@ -1113,9 +1114,7 @@ namespace Discord var user = guild.GetUser(data.UserId); if (user != null) - { await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); - } else { await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); @@ -1131,7 +1130,21 @@ namespace Discord } break; case "VOICE_SERVER_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + + if (AudioMode != AudioMode.Disabled) + { + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await guild.ConnectAudio("wss://" + data.Endpoint, data.Token).ConfigureAwait(false); + else + { + await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + return; //Ignored (User only) @@ -1183,7 +1196,7 @@ namespace Discord if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) { await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); return; } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 2f383b4bc..22b2df52b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ChannelModel = Discord.API.Channel; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; @@ -17,8 +18,9 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { - internal class CachedGuild : Guild, IUserGuild, ICachedEntity + internal class CachedGuild : Guild, ICachedEntity, IGuild, IUserGuild { + private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; @@ -27,7 +29,7 @@ namespace Discord public bool Available { get; private set; } public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } - public IAudioClient AudioClient { get; private set; } + public AudioClient AudioClient { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task DownloaderPromise => _downloaderPromise.Task; @@ -48,6 +50,7 @@ namespace Discord public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { + _audioLock = new SemaphoreSlim(1, 1); _downloaderPromise = new TaskCompletionSource(); Update(model, UpdateSource.Creation, dataStore); } @@ -236,6 +239,55 @@ namespace Discord return null; } + public async Task ConnectAudio(string url, string token) + { + AudioClient audioClient; + await _audioLock.WaitAsync().ConfigureAwait(false); + var voiceState = GetVoiceState(CurrentUser.Id).Value; + try + { + audioClient = AudioClient; + if (audioClient == null) + { + audioClient = new AudioClient(this); + audioClient.Disconnected += async ex => + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (ex != null) + { + //Reconnect if we still have channel info. + //TODO: Is this threadsafe? Could channel data be deleted before we access it? + var voiceState2 = GetVoiceState(CurrentUser.Id); + if (voiceState2.HasValue) + { + var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; + if (voiceChannelId != null) + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); + } + } + else + { + try { AudioClient.Dispose(); } catch { } + AudioClient = null; + } + } + finally + { + _audioLock.Release(); + } + }; + AudioClient = audioClient; + } + } + finally + { + _audioLock.Release(); + } + await audioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; new internal ICachedGuildChannel ToChannel(ChannelModel model) @@ -253,5 +305,6 @@ namespace Discord bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; + IAudioClient IGuild.AudioClient => AudioClient; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index c65d7b49e..796eb2611 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,4 +1,5 @@ using Discord.API; +using Discord.Logging; using System; using System.Collections.Generic; using System.IO; @@ -13,6 +14,7 @@ namespace Discord ConnectionState ConnectionState { get; } DiscordApiClient ApiClient { get; } + ILogManager LogManager { get; } Task LoginAsync(TokenType tokenType, string token, bool validateToken = true); Task LogoutAsync(); From 09572b0f76e590342751bd70e2c437815433e521 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 08:44:55 -0300 Subject: [PATCH 143/160] Fixed using wrong port for WSS audio connections --- src/Discord.Net/DiscordSocketClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index eb1096988..4abc901d7 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -1137,7 +1137,10 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); if (guild != null) - await guild.ConnectAudio("wss://" + data.Endpoint, data.Token).ConfigureAwait(false); + { + string endpoint = "wss://" + data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); + await guild.ConnectAudio(endpoint, data.Token).ConfigureAwait(false); + } else { await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); From 6d6af08815a075edeed57f6278e4f6c61e0ff5c8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 17:30:18 -0300 Subject: [PATCH 144/160] Renamed DownloadAllMembers -> DownloadAllUsers, fixed overriding CachedGuild's DownloadUsers --- src/Discord.Net/DiscordSocketClient.cs | 12 ++++++------ src/Discord.Net/Entities/Guilds/Guild.cs | 5 ++++- src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 4abc901d7..82289fe95 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -362,11 +362,11 @@ namespace Discord return DataStore.RemoveUser(id); } - /// Downloads the members list for all large guilds. - public Task DownloadAllMembersAsync() - => DownloadMembersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); - /// Downloads the members list for the provided guilds, if they don't have a complete list. - public async Task DownloadMembersAsync(IEnumerable guilds) + /// Downloads the users list for all large guilds. + public Task DownloadAllUsersAsync() + => DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); + /// Downloads the users list for the provided guilds, if they don't have a complete list. + public async Task DownloadUsersAsync(IEnumerable guilds) { const short batchSize = 50; var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); @@ -374,7 +374,7 @@ namespace Discord return; else if (cachedGuilds.Length == 1) { - await cachedGuilds[0].DownloadMembersAsync().ConfigureAwait(false); + await cachedGuilds[0].DownloadUsersAsync().ConfigureAwait(false); return; } diff --git a/src/Discord.Net/Entities/Guilds/Guild.cs b/src/Discord.Net/Entities/Guilds/Guild.cs index 31a0f5916..0031fad85 100644 --- a/src/Discord.Net/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Entities/Guilds/Guild.cs @@ -289,6 +289,10 @@ namespace Discord model = await Discord.ApiClient.BeginGuildPruneAsync(Id, args).ConfigureAwait(false); return model.Pruned; } + public virtual Task DownloadUsersAsync() + { + throw new NotSupportedException(); + } internal GuildChannel ToChannel(API.Channel model) { @@ -311,7 +315,6 @@ namespace Discord IRole IGuild.EveryoneRole => EveryoneRole; IReadOnlyCollection IGuild.Emojis => Emojis; IReadOnlyCollection IGuild.Features => Features; - Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } IAudioClient IGuild.AudioClient => null; IRole IGuild.GetRole(ulong id) => GetRole(id); diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 22b2df52b..e1641be03 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -206,7 +206,7 @@ namespace Discord return member; return null; } - public async Task DownloadMembersAsync() + public override async Task DownloadUsersAsync() { if (!HasAllMembers) await Discord.ApiClient.SendRequestMembersAsync(new ulong[] { Id }).ConfigureAwait(false); From 4a934ee16ca30f2b5933d7d744215c3b06de3c53 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 17:54:41 -0300 Subject: [PATCH 145/160] Added DebuggerDisplay to ConcurrentDictionaryWrapper --- src/Discord.Net/Extensions/CollectionExtensions.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/Extensions/CollectionExtensions.cs b/src/Discord.Net/Extensions/CollectionExtensions.cs index 6c81fe9cd..921379bfc 100644 --- a/src/Discord.Net/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net/Extensions/CollectionExtensions.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Discord.Extensions @@ -11,7 +12,8 @@ namespace Discord.Extensions public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new ConcurrentDictionaryWrapper(source, query); } - + + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal struct ConcurrentDictionaryWrapper : IReadOnlyCollection { private readonly IReadOnlyCollection _source; @@ -26,6 +28,8 @@ namespace Discord.Extensions _query = query; } + private string DebuggerDisplay => $"Count = {Count}"; + public IEnumerator GetEnumerator() => _query.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _query.GetEnumerator(); } From 8da291357688514a8cf7cbe78c0118e5e50d64b7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 19:11:57 -0300 Subject: [PATCH 146/160] Added support for GUILD_SYNC --- src/Discord.Net/API/DiscordAPIClient.cs | 4 + src/Discord.Net/API/Gateway/GatewayOpCode.cs | 6 +- src/Discord.Net/API/Gateway/GuildSyncEvent.cs | 17 ++++ src/Discord.Net/DiscordSocketClient.cs | 93 ++++++++++++++++--- .../Entities/WebSocket/CachedGuild.cs | 45 +++++++-- 5 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 src/Discord.Net/API/Gateway/GuildSyncEvent.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 128c15474..c2a439b06 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -409,6 +409,10 @@ namespace Discord.API }; await SendGatewayAsync(GatewayOpCode.VoiceStateUpdate, payload, options: options).ConfigureAwait(false); } + public async Task SendGuildSyncAsync(IEnumerable guildIds, RequestOptions options = null) + { + await SendGatewayAsync(GatewayOpCode.GuildSync, guildIds, options: options).ConfigureAwait(false); + } //Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net/API/Gateway/GatewayOpCode.cs b/src/Discord.Net/API/Gateway/GatewayOpCode.cs index dc69d073c..3c2a3382b 100644 --- a/src/Discord.Net/API/Gateway/GatewayOpCode.cs +++ b/src/Discord.Net/API/Gateway/GatewayOpCode.cs @@ -18,13 +18,15 @@ 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 + /// C→S - Used to request members that were withheld by large_threshold RequestGuildMembers = 8, /// C←S - Used to notify the client that their session has expired and cannot be resumed. InvalidSession = 9, /// C←S - Used to provide information to the client immediately on connection. Hello = 10, /// C←S - Used to reply to a client's heartbeat. - HeartbeatAck = 11 + HeartbeatAck = 11, + /// C→S - Used to request presence updates from particular guilds. + GuildSync = 12 } } diff --git a/src/Discord.Net/API/Gateway/GuildSyncEvent.cs b/src/Discord.Net/API/Gateway/GuildSyncEvent.cs new file mode 100644 index 000000000..ff290f23a --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildSyncEvent.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildSyncEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("large")] + public bool Large { get; set; } + + [JsonProperty("presences")] + public Presence[] Presences { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 82289fe95..3b2ac8137 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -29,6 +29,8 @@ namespace Discord private int _lastSeq; private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; + private ConcurrentHashSet _syncedGuilds; + private SemaphoreSlim _syncedGuildsLock; private CancellationTokenSource _cancelToken; private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; private long _heartbeatTime; @@ -102,6 +104,8 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); + _syncedGuilds = new ConcurrentHashSet(); + _syncedGuildsLock = new SemaphoreSlim(1, 1); } protected override async Task OnLoginAsync() @@ -295,7 +299,7 @@ namespace Discord { return Task.FromResult>(Guilds); } - internal CachedGuild AddGuild(API.Gateway.ExtendedGuild model, DataStore dataStore) + internal CachedGuild AddGuild(ExtendedGuild model, DataStore dataStore) { var guild = new CachedGuild(this, model, dataStore); dataStore.AddGuild(guild); @@ -305,6 +309,7 @@ namespace Discord } internal CachedGuild RemoveGuild(ulong id) { + _syncedGuilds.TryRemove(id); var guild = DataStore.RemoveGuild(id); foreach (var channel in guild.Channels) guild.RemoveChannel(channel.Id); @@ -363,18 +368,47 @@ namespace Discord } /// Downloads the users list for all large guilds. - public Task DownloadAllUsersAsync() + public Task DownloadAllUsersAsync() => DownloadUsersAsync(DataStore.Guilds.Where(x => !x.HasAllMembers)); /// Downloads the users list for the provided guilds, if they don't have a complete list. - public async Task DownloadUsersAsync(IEnumerable guilds) + public Task DownloadUsersAsync(IEnumerable guilds) + => DownloadUsersAsync(guilds.Select(x => x as CachedGuild).Where(x => x != null)); + public Task DownloadUsersAsync(params IGuild[] guilds) + => DownloadUsersAsync(guilds.Select(x => x as CachedGuild).Where(x => x != null)); + private async Task DownloadUsersAsync(IEnumerable guilds) { + var cachedGuilds = guilds.ToArray(); + if (cachedGuilds.Length == 0) return; + + //Sync guilds + if (ApiClient.AuthTokenType == TokenType.User) + { + await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); + try + { + foreach (var guild in cachedGuilds) + _syncedGuilds.TryAdd(guild.Id); + await ApiClient.SendGuildSyncAsync(_syncedGuilds).ConfigureAwait(false); + await Task.WhenAll(cachedGuilds.Select(x => x.SyncPromise)); + + //Reduce the list only to those with members left to download + cachedGuilds = cachedGuilds.Where(x => !x.HasAllMembers).ToArray(); + if (cachedGuilds.Length == 0) return; + } + finally + { + _syncedGuildsLock.Release(); + } + } + + //Download offline members const short batchSize = 50; - var cachedGuilds = guilds.Select(x => x as CachedGuild).ToArray(); - if (cachedGuilds.Length == 0) - return; - else if (cachedGuilds.Length == 1) + + if (cachedGuilds.Length == 1) { - await cachedGuilds[0].DownloadUsersAsync().ConfigureAwait(false); + if (!cachedGuilds[0].HasAllMembers) + await ApiClient.SendRequestMembersAsync(new ulong[] { cachedGuilds[0].Id }).ConfigureAwait(false); + await cachedGuilds[0].DownloaderPromise.ConfigureAwait(false); return; } @@ -502,6 +536,15 @@ namespace Discord _currentUser = currentUser; _unavailableGuilds = unavailableGuilds; _lastGuildAvailableTime = Environment.TickCount; + await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); + try + { + _syncedGuilds = new ConcurrentHashSet(); + } + finally + { + _syncedGuildsLock.Release(); + } DataStore = dataStore; _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); @@ -513,9 +556,11 @@ namespace Discord } break; case "RESUMED": - await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); + { + await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false); - await _gatewayLogger.InfoAsync("Resume").ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); + } return; //Guilds @@ -579,9 +624,9 @@ namespace Discord } } break; - case "GUILD_EMOJI_UPDATE": //TODO: Add + case "GUILD_EMOJIS_UPDATE": //TODO: Add { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJI_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); var guild = DataStore.GetGuild(data.GuildId); @@ -593,13 +638,33 @@ namespace Discord } else { - await _gatewayLogger.WarningAsync("GUILD_EMOJI_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await _gatewayLogger.WarningAsync("GUILD_EMOJIS_UPDATE referenced an unknown guild.").ConfigureAwait(false); return; } } return; case "GUILD_INTEGRATIONS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + { + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); + } + return; + case "GUILD_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.Id); + if (guild != null) + { + var before = guild.Clone(); + guild.Update(data, UpdateSource.WebSocket, DataStore); + await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + } + else + { + await _gatewayLogger.WarningAsync("GUILD_SYNC referenced an unknown guild.").ConfigureAwait(false); + return; + } + } return; case "GUILD_DELETE": { diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index e1641be03..88db46763 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using ChannelModel = Discord.API.Channel; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using ExtendedModel = Discord.API.Gateway.ExtendedGuild; +using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; @@ -18,10 +19,16 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { + internal enum MemberDownloadState + { + Incomplete, + Synced, + Complete + } internal class CachedGuild : Guild, ICachedEntity, IGuild, IUserGuild { private readonly SemaphoreSlim _audioLock; - private TaskCompletionSource _downloaderPromise; + private TaskCompletionSource _syncPromise, _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _voiceStates; @@ -29,9 +36,11 @@ namespace Discord public bool Available { get; private set; } public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } - public AudioClient AudioClient { get; private set; } + public AudioClient AudioClient { get; private set; } + public MemberDownloadState MemberDownloadState { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; + public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; @@ -51,6 +60,7 @@ namespace Discord public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { _audioLock = new SemaphoreSlim(1, 1); + _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); Update(model, UpdateSource.Creation, dataStore); } @@ -91,9 +101,12 @@ namespace Discord DownloadedMemberCount = 0; for (int i = 0; i < model.Members.Length; i++) AddUser(model.Members[i], dataStore, members); - _downloaderPromise = new TaskCompletionSource(); - if (!model.Large) - _downloaderPromise.SetResult(true); + if (Discord.ApiClient.AuthTokenType != TokenType.User) + { + _syncPromise.TrySetResult(true); + if (!model.Large) + _downloaderPromise.TrySetResult(true); + } for (int i = 0; i < model.Presences.Length; i++) AddOrUpdateUser(model.Presences[i], dataStore, members); @@ -107,6 +120,24 @@ namespace Discord } _voiceStates = voiceStates; } + public void Update(GuildSyncModel model, UpdateSource source, DataStore dataStore) + { + if (source == UpdateSource.Rest && IsAttached) return; + + var members = new ConcurrentDictionary(1, (int)(model.Presences.Length * 1.05)); + { + DownloadedMemberCount = 0; + for (int i = 0; i < model.Members.Length; i++) + AddUser(model.Members[i], dataStore, members); + _syncPromise.TrySetResult(true); + if (!model.Large) + _downloaderPromise.TrySetResult(true); + + for (int i = 0; i < model.Presences.Length; i++) + AddOrUpdateUser(model.Presences[i], dataStore, members); + } + _members = members; + } public void Update(EmojiUpdateModel model, UpdateSource source) { @@ -208,9 +239,7 @@ namespace Discord } public override async Task DownloadUsersAsync() { - if (!HasAllMembers) - await Discord.ApiClient.SendRequestMembersAsync(new ulong[] { Id }).ConfigureAwait(false); - await _downloaderPromise.Task.ConfigureAwait(false); + await Discord.DownloadUsersAsync(new [] { this }); } public void CompleteDownloadMembers() { From 1a40f62249ab19f6bb5b43d5a5852efb4631cc70 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 19:13:33 -0300 Subject: [PATCH 147/160] Removed unused enum --- src/Discord.Net/Entities/WebSocket/CachedGuild.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 88db46763..d75ba4bab 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -19,12 +19,6 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { - internal enum MemberDownloadState - { - Incomplete, - Synced, - Complete - } internal class CachedGuild : Guild, ICachedEntity, IGuild, IUserGuild { private readonly SemaphoreSlim _audioLock; @@ -37,7 +31,6 @@ namespace Discord public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } public AudioClient AudioClient { get; private set; } - public MemberDownloadState MemberDownloadState { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; From c6157a086873d31f8f55996b4a460da3c4b0cab5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 19:35:14 -0300 Subject: [PATCH 148/160] Default new CachedGuildUsers to offline --- src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 86036d188..0b84d227b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -38,6 +38,7 @@ namespace Discord public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, Model model) : base(guild, user, model) { + Presence = new Presence(null, UserStatus.Offline); } public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, PresenceModel model) : base(guild, user, model) From 9e8b8975899710d1f882929007eadd279b68febb Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 20:05:57 -0300 Subject: [PATCH 149/160] Added ISelfUser.ModifyStatusAsync --- src/Discord.Net/API/DiscordAPIClient.cs | 9 ++++++ .../API/Gateway/StatusUpdateParams.cs | 12 +++++++ .../API/Rest/ModifyPresenceParams.cs | 8 +++++ src/Discord.Net/Entities/Users/ISelfUser.cs | 1 + src/Discord.Net/Entities/Users/SelfUser.cs | 31 ++++++++++++++++++- 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net/API/Gateway/StatusUpdateParams.cs create mode 100644 src/Discord.Net/API/Rest/ModifyPresenceParams.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index c2a439b06..55f0cc009 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -394,6 +394,15 @@ namespace Discord.API { await SendGatewayAsync(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); } + public async Task SendStatusUpdateAsync(long? idleSince, Game game, RequestOptions options = null) + { + var args = new StatusUpdateParams + { + IdleSince = idleSince, + Game = game + }; + await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); + } public async Task SendRequestMembersAsync(IEnumerable guildIds, RequestOptions options = null) { await SendGatewayAsync(GatewayOpCode.RequestGuildMembers, new RequestMembersParams { GuildIds = guildIds, Query = "", Limit = 0 }, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net/API/Gateway/StatusUpdateParams.cs b/src/Discord.Net/API/Gateway/StatusUpdateParams.cs new file mode 100644 index 000000000..29a525674 --- /dev/null +++ b/src/Discord.Net/API/Gateway/StatusUpdateParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class StatusUpdateParams + { + [JsonProperty("idle_since"), Int53] + public long? IdleSince { get; set; } + [JsonProperty("game")] + public Game Game { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyPresenceParams.cs b/src/Discord.Net/API/Rest/ModifyPresenceParams.cs new file mode 100644 index 000000000..6c6579e4d --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyPresenceParams.cs @@ -0,0 +1,8 @@ +namespace Discord.API.Rest +{ + public class ModifyPresenceParams + { + public Optional Status { get; set; } + public Optional Game { get; set; } + } +} diff --git a/src/Discord.Net/Entities/Users/ISelfUser.cs b/src/Discord.Net/Entities/Users/ISelfUser.cs index 40f43b019..b6803ccf6 100644 --- a/src/Discord.Net/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net/Entities/Users/ISelfUser.cs @@ -14,5 +14,6 @@ namespace Discord bool IsMfaEnabled { get; } Task ModifyAsync(Action func); + Task ModifyStatusAsync(Action func); } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs index 393a1ad3a..0e696aea4 100644 --- a/src/Discord.Net/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -6,11 +6,18 @@ using Model = Discord.API.User; namespace Discord { internal class SelfUser : User, ISelfUser - { + { + private long _idleSince; + private UserStatus _status; + private Game _game; + public string Email { get; private set; } public bool IsVerified { get; private set; } public bool IsMfaEnabled { get; private set; } + public override UserStatus Status => _status; + public override Game Game => _game; + public override DiscordClient Discord { get; } public SelfUser(DiscordClient discord, Model model) @@ -49,5 +56,27 @@ namespace Discord var model = await Discord.ApiClient.ModifySelfAsync(args).ConfigureAwait(false); Update(model, UpdateSource.Rest); } + public async Task ModifyStatusAsync(Action func) + { + if (func == null) throw new NullReferenceException(nameof(func)); + + var args = new ModifyPresenceParams(); + func(args); + + var game = args.Game.GetValueOrDefault(_game); + var status = args.Status.GetValueOrDefault(_status); + + long idleSince = _idleSince; + if (status == UserStatus.Idle && _status != UserStatus.Idle) + idleSince = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var apiGame = new API.Game { Name = game.Name, StreamType = game.StreamType, StreamUrl = game.StreamUrl }; + + await Discord.ApiClient.SendStatusUpdateAsync(status == UserStatus.Idle ? _idleSince : (long?)null, apiGame).ConfigureAwait(false); + + //Save values + _idleSince = idleSince; + _game = game; + _status = status; + } } } From d44163791f2b10e689dbb5d07b676c540f32851a Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 20:40:05 -0300 Subject: [PATCH 150/160] Automatically sync guilds --- src/Discord.Net/DiscordSocketClient.cs | 48 +++++++------------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 3b2ac8137..7bb2e93a6 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -29,8 +29,6 @@ namespace Discord private int _lastSeq; private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; - private ConcurrentHashSet _syncedGuilds; - private SemaphoreSlim _syncedGuildsLock; private CancellationTokenSource _cancelToken; private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; private long _heartbeatTime; @@ -104,8 +102,6 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); - _syncedGuilds = new ConcurrentHashSet(); - _syncedGuildsLock = new SemaphoreSlim(1, 1); } protected override async Task OnLoginAsync() @@ -309,7 +305,6 @@ namespace Discord } internal CachedGuild RemoveGuild(ulong id) { - _syncedGuilds.TryRemove(id); var guild = DataStore.RemoveGuild(id); foreach (var channel in guild.Channels) guild.RemoveChannel(channel.Id); @@ -380,26 +375,9 @@ namespace Discord var cachedGuilds = guilds.ToArray(); if (cachedGuilds.Length == 0) return; - //Sync guilds - if (ApiClient.AuthTokenType == TokenType.User) - { - await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); - try - { - foreach (var guild in cachedGuilds) - _syncedGuilds.TryAdd(guild.Id); - await ApiClient.SendGuildSyncAsync(_syncedGuilds).ConfigureAwait(false); - await Task.WhenAll(cachedGuilds.Select(x => x.SyncPromise)); - - //Reduce the list only to those with members left to download - cachedGuilds = cachedGuilds.Where(x => !x.HasAllMembers).ToArray(); - if (cachedGuilds.Length == 0) return; - } - finally - { - _syncedGuildsLock.Release(); - } - } + var unsyncedGuilds = guilds.Select(x => x.SyncPromise).Where(x => !x.IsCompleted).ToArray(); + if (unsyncedGuilds.Length > 0) + await Task.WhenAll(unsyncedGuilds); //Download offline members const short batchSize = 50; @@ -521,7 +499,6 @@ namespace Discord var currentUser = new CachedSelfUser(this, data.User); int unavailableGuilds = 0; //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); - for (int i = 0; i < data.Guilds.Length; i++) { var model = data.Guilds[i]; @@ -536,20 +513,12 @@ namespace Discord _currentUser = currentUser; _unavailableGuilds = unavailableGuilds; _lastGuildAvailableTime = Environment.TickCount; - await _syncedGuildsLock.WaitAsync().ConfigureAwait(false); - try - { - _syncedGuilds = new ConcurrentHashSet(); - } - finally - { - _syncedGuildsLock.Release(); - } - DataStore = dataStore; + DataStore = dataStore; _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); await _readyEvent.InvokeAsync().ConfigureAwait(false); + await SyncGuildsAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); @@ -579,6 +548,7 @@ namespace Discord if (data.Unavailable != false) { guild = AddGuild(data, DataStore); + await SyncGuildsAsync().ConfigureAwait(false); await _joinedGuildEvent.InvokeAsync(guild).ConfigureAwait(false); await _gatewayLogger.InfoAsync($"Joined {data.Name}").ConfigureAwait(false); } @@ -1280,5 +1250,11 @@ namespace Discord while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); } + private async Task SyncGuildsAsync() + { + var guildIds = Guilds.Where(x => x.Available).Select(x => x.Id).ToArray(); + if (guildIds.Length > 0) + await ApiClient.SendGuildSyncAsync(guildIds).ConfigureAwait(false); + } } } From 689c9ff31ae051caddea538cad9f63d003d26200 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 28 Jun 2016 23:33:28 -0300 Subject: [PATCH 151/160] Finished AudioClient connection handshake --- src/Discord.Net/API/DiscordAPIClient.cs | 6 +- src/Discord.Net/API/DiscordVoiceAPIClient.cs | 114 ++++++++++++++---- src/Discord.Net/API/Voice/ReadyEvent.cs | 16 +++ .../API/Voice/SelectProtocolParams.cs | 12 ++ .../API/Voice/SessionDescriptionEvent.cs | 12 ++ src/Discord.Net/API/Voice/SpeakingParams.cs | 12 ++ src/Discord.Net/API/Voice/UdpProtocolInfo.cs | 14 +++ src/Discord.Net/Audio/AudioClient.cs | 99 +++++++++++---- src/Discord.Net/DiscordClient.cs | 3 +- src/Discord.Net/DiscordSocketClient.cs | 8 +- .../Entities/WebSocket/CachedGuild.cs | 4 +- src/Discord.Net/project.json | 2 + 12 files changed, 247 insertions(+), 55 deletions(-) create mode 100644 src/Discord.Net/API/Voice/ReadyEvent.cs create mode 100644 src/Discord.Net/API/Voice/SelectProtocolParams.cs create mode 100644 src/Discord.Net/API/Voice/SessionDescriptionEvent.cs create mode 100644 src/Discord.Net/API/Voice/SpeakingParams.cs create mode 100644 src/Discord.Net/API/Voice/UdpProtocolInfo.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index 55f0cc009..a8644f97b 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -27,8 +27,8 @@ namespace Discord.API public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); - public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } - private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); public event Func ReceivedGatewayEvent { add { _receivedGatewayEvent.Add(value); } remove { _receivedGatewayEvent.Remove(value); } } private readonly AsyncEvent> _receivedGatewayEvent = new AsyncEvent>(); @@ -352,7 +352,7 @@ namespace Discord.API if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); await _requestQueue.SendAsync(new WebSocketRequest(_gatewayClient, bytes, true, options), group, bucketId, guildId).ConfigureAwait(false); - await _sentGatewayMessageEvent.InvokeAsync((int)opCode).ConfigureAwait(false); + await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } //Auth diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs index ddb8f2b6b..ecbd47778 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -11,28 +11,37 @@ using System.IO.Compression; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Net.Sockets; +using System.Net; namespace Discord.Audio { public class DiscordVoiceAPIClient { public const int MaxBitrate = 128; - private const string Mode = "xsalsa20_poly1305"; + public const string Mode = "xsalsa20_poly1305"; public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); - public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } - private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func SentGatewayMessage { add { _sentGatewayMessageEvent.Add(value); } remove { _sentGatewayMessageEvent.Remove(value); } } + private readonly AsyncEvent> _sentGatewayMessageEvent = new AsyncEvent>(); + public event Func SentDiscovery { add { _sentDiscoveryEvent.Add(value); } remove { _sentDiscoveryEvent.Remove(value); } } + private readonly AsyncEvent> _sentDiscoveryEvent = new AsyncEvent>(); public event Func ReceivedEvent { add { _receivedEvent.Add(value); } remove { _receivedEvent.Remove(value); } } private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); + public event Func ReceivedPacket { add { _receivedPacketEvent.Add(value); } remove { _receivedPacketEvent.Remove(value); } } + private readonly AsyncEvent> _receivedPacketEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); private readonly JsonSerializer _serializer; - private readonly IWebSocketClient _gatewayClient; + private readonly IWebSocketClient _webSocketClient; private readonly SemaphoreSlim _connectionLock; private CancellationTokenSource _connectCancelToken; + private UdpClient _udp; + private IPEndPoint _udpEndpoint; + private Task _udpRecieveTask; private bool _isDisposed; public ulong GuildId { get; } @@ -42,10 +51,11 @@ namespace Discord.Audio { GuildId = guildId; _connectionLock = new SemaphoreSlim(1, 1); + _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); - _gatewayClient = webSocketProvider(); + _webSocketClient = webSocketProvider(); //_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+) - _gatewayClient.BinaryMessage += async (data, index, count) => + _webSocketClient.BinaryMessage += async (data, index, count) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var decompressed = new MemoryStream()) @@ -60,12 +70,12 @@ namespace Discord.Audio } } }; - _gatewayClient.TextMessage += async text => + _webSocketClient.TextMessage += async text => { var msg = JsonConvert.DeserializeObject(text); await _receivedEvent.InvokeAsync((VoiceOpCode)msg.Operation, msg.Payload).ConfigureAwait(false); }; - _gatewayClient.Closed += async ex => + _webSocketClient.Closed += async ex => { await DisconnectAsync().ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); @@ -80,21 +90,29 @@ namespace Discord.Audio if (disposing) { _connectCancelToken?.Dispose(); - (_gatewayClient as IDisposable)?.Dispose(); + (_webSocketClient as IDisposable)?.Dispose(); } _isDisposed = true; } } public void Dispose() => Dispose(true); - public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) + public async Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) { byte[] bytes = null; payload = new WebSocketMessage { Operation = (int)opCode, Payload = payload }; if (payload != null) bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); - //TODO: Send - return Task.CompletedTask; + await _webSocketClient.SendAsync(bytes, 0, bytes.Length, true).ConfigureAwait(false); + await _sentGatewayMessageEvent.InvokeAsync(opCode); + } + public async Task SendAsync(byte[] data, int bytes) + { + if (_udpEndpoint != null) + { + await _udp.SendAsync(data, bytes, _udpEndpoint).ConfigureAwait(false); + await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); + } } //WebSocket @@ -102,36 +120,56 @@ namespace Discord.Audio { await SendAsync(VoiceOpCode.Heartbeat, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), options: options).ConfigureAwait(false); } - public async Task SendIdentityAsync(ulong guildId, ulong userId, string sessionId, string token) + public async Task SendIdentityAsync(ulong userId, string sessionId, string token) { await SendAsync(VoiceOpCode.Identify, new IdentifyParams { - GuildId = guildId, + GuildId = GuildId, UserId = userId, SessionId = sessionId, Token = token }); } + public async Task SendSelectProtocol(string externalIp, int externalPort) + { + await SendAsync(VoiceOpCode.SelectProtocol, new SelectProtocolParams + { + Protocol = "udp", + Data = new UdpProtocolInfo + { + Address = externalIp, + Port = externalPort, + Mode = Mode + } + }); + } + public async Task SendSetSpeaking(bool value) + { + await SendAsync(VoiceOpCode.Speaking, new SpeakingParams + { + IsSpeaking = value, + Delay = 0 + }); + } - public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) + public async Task ConnectAsync(string url) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); + await ConnectInternalAsync(url).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) + private async Task ConnectInternalAsync(string url) { ConnectionState = ConnectionState.Connecting; try { _connectCancelToken = new CancellationTokenSource(); - _gatewayClient.SetCancelToken(_connectCancelToken.Token); - await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); - - await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false); + _webSocketClient.SetCancelToken(_connectCancelToken.Token); + await _webSocketClient.ConnectAsync(url).ConfigureAwait(false); + _udpRecieveTask = ReceiveAsync(_connectCancelToken.Token); ConnectionState = ConnectionState.Connected; } @@ -159,11 +197,43 @@ namespace Discord.Audio try { _connectCancelToken?.Cancel(false); } catch { } - await _gatewayClient.DisconnectAsync().ConfigureAwait(false); + //Wait for tasks to complete + await _udpRecieveTask.ConfigureAwait(false); + + await _webSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } + //Udp + public async Task SendDiscoveryAsync(uint ssrc) + { + var packet = new byte[70]; + packet[0] = (byte)(ssrc >> 24); + packet[1] = (byte)(ssrc >> 16); + packet[2] = (byte)(ssrc >> 8); + packet[3] = (byte)(ssrc >> 0); + await SendAsync(packet, 70).ConfigureAwait(false); + } + + public void SetUdpEndpoint(IPEndPoint endpoint) + { + _udpEndpoint = endpoint; + } + private async Task ReceiveAsync(CancellationToken cancelToken) + { + var closeTask = Task.Delay(-1, cancelToken); + while (!cancelToken.IsCancellationRequested) + { + var receiveTask = _udp.ReceiveAsync(); + var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); + if (task == closeTask) + break; + + await _receivedPacketEvent.InvokeAsync(receiveTask.Result.Buffer).ConfigureAwait(false); + } + } + //Helpers private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); private string SerializeJson(object value) diff --git a/src/Discord.Net/API/Voice/ReadyEvent.cs b/src/Discord.Net/API/Voice/ReadyEvent.cs new file mode 100644 index 000000000..7914fd825 --- /dev/null +++ b/src/Discord.Net/API/Voice/ReadyEvent.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class ReadyEvent + { + [JsonProperty("ssrc")] + public uint SSRC { get; set; } + [JsonProperty("port")] + public ushort Port { get; set; } + [JsonProperty("modes")] + public string[] Modes { get; set; } + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/SelectProtocolParams.cs b/src/Discord.Net/API/Voice/SelectProtocolParams.cs new file mode 100644 index 000000000..b7992ba5a --- /dev/null +++ b/src/Discord.Net/API/Voice/SelectProtocolParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class SelectProtocolParams + { + [JsonProperty("protocol")] + public string Protocol { get; set; } + [JsonProperty("data")] + public UdpProtocolInfo Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/SessionDescriptionEvent.cs b/src/Discord.Net/API/Voice/SessionDescriptionEvent.cs new file mode 100644 index 000000000..6a2846f8f --- /dev/null +++ b/src/Discord.Net/API/Voice/SessionDescriptionEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class SessionDescriptionEvent + { + [JsonProperty("secret_key")] + public byte[] SecretKey { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/SpeakingParams.cs b/src/Discord.Net/API/Voice/SpeakingParams.cs new file mode 100644 index 000000000..01fd001b0 --- /dev/null +++ b/src/Discord.Net/API/Voice/SpeakingParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class SpeakingParams + { + [JsonProperty("speaking")] + public bool IsSpeaking { get; set; } + [JsonProperty("delay")] + public int Delay { get; set; } + } +} diff --git a/src/Discord.Net/API/Voice/UdpProtocolInfo.cs b/src/Discord.Net/API/Voice/UdpProtocolInfo.cs new file mode 100644 index 000000000..d31bbd8db --- /dev/null +++ b/src/Discord.Net/API/Voice/UdpProtocolInfo.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + public class UdpProtocolInfo + { + [JsonProperty("address")] + public string Address { get; set; } + [JsonProperty("port")] + public int Port { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } +} diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index b59f3d267..210cce347 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -2,7 +2,11 @@ using Discord.Logging; using Discord.Net.Converters; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; +using System.Linq; +using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -29,7 +33,7 @@ namespace Discord.Audio } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - private readonly ILogger _webSocketLogger, _udpLogger; + private readonly ILogger _audioLogger; #if BENCHMARK private readonly ILogger _benchmarkLogger; #endif @@ -42,6 +46,8 @@ namespace Discord.Audio private long _heartbeatTime; private string _url; private bool _isDisposed; + private uint _ssrc; + private byte[] _secretKey; public CachedGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } @@ -51,12 +57,11 @@ namespace Discord.Audio private DiscordSocketClient Discord => Guild.Discord; /// Creates a new REST/WebSocket discord client. - internal AudioClient(CachedGuild guild) + internal AudioClient(CachedGuild guild, int id) { Guild = guild; - _webSocketLogger = Discord.LogManager.CreateLogger("Audio"); - _udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); + _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); #if BENCHMARK _benchmarkLogger = logManager.CreateLogger("Benchmark"); #endif @@ -66,20 +71,22 @@ namespace Discord.Audio _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { - _webSocketLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); + _audioLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult(); e.ErrorContext.Handled = true; }; ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); - ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); + ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); + ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; + ApiClient.ReceivedPacket += ProcessPacketAsync; ApiClient.Disconnected += async ex => { if (ex != null) - await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); + await _audioLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); else - await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); + await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; } @@ -100,19 +107,20 @@ namespace Discord.Audio await DisconnectInternalAsync(null).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; - await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); + await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false); try { _url = url; _connectTask = new TaskCompletionSource(); _cancelToken = new CancellationTokenSource(); - await ApiClient.ConnectAsync(url, userId, sessionId, token).ConfigureAwait(false); - await _connectedEvent.InvokeAsync().ConfigureAwait(false); + await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false); + await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); + await _connectedEvent.InvokeAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Connected; - await _webSocketLogger.InfoAsync("Connected").ConfigureAwait(false); + await _audioLogger.InfoAsync("Connected").ConfigureAwait(false); } catch (Exception) { @@ -143,7 +151,7 @@ namespace Discord.Audio { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - await _webSocketLogger.InfoAsync("Disconnecting").ConfigureAwait(false); + await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false); //Signal tasks to complete try { _cancelToken.Cancel(); } catch { } @@ -158,7 +166,7 @@ namespace Discord.Audio _heartbeatTask = null; ConnectionState = ConnectionState.Disconnected; - await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); + await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false); await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } @@ -174,25 +182,49 @@ namespace Discord.Audio { switch (opCode) { - /*case VoiceOpCode.Ready: + case VoiceOpCode.Ready: { - await _webSocketLogger.DebugAsync("Received Ready").ConfigureAwait(false); + await _audioLogger.DebugAsync("Received Ready").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - + + _ssrc = data.SSRC; + + if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) + throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); + _heartbeatTime = 0; _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); + + var entry = await Dns.GetHostEntryAsync(_url).ConfigureAwait(false); + + ApiClient.SetUdpEndpoint(new IPEndPoint(entry.AddressList[0], data.Port)); + await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); } - break;*/ + break; + case VoiceOpCode.SessionDescription: + { + await _audioLogger.DebugAsync("Received SessionDescription").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + + if (data.Mode != DiscordVoiceAPIClient.Mode) + throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); + + _secretKey = data.SecretKey; + await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + + _connectTask.TrySetResult(true); + } + break; case VoiceOpCode.HeartbeatAck: { - await _webSocketLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); + await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); var heartbeatTime = _heartbeatTime; if (heartbeatTime != 0) { int latency = (int)(Environment.TickCount - _heartbeatTime); _heartbeatTime = 0; - await _webSocketLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + await _audioLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); int before = Latency; Latency = latency; @@ -202,13 +234,13 @@ namespace Discord.Audio } break; default: - await _webSocketLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); + await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); return; } } catch (Exception ex) { - await _webSocketLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); + await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false); return; } #if BENCHMARK @@ -222,6 +254,27 @@ namespace Discord.Audio #endif } + private async Task ProcessPacketAsync(byte[] packet) + { + if (!_connectTask.Task.IsCompleted) + { + if (packet.Length == 70) + { + string ip; + int port; + try + { + ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); + port = packet[68] | packet[69] << 8; + } + catch { return; } + + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); + await ApiClient.SendSelectProtocol(ip, port); + } + } + } + private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken) { //Clean this up when Discord's session patch is live @@ -235,7 +288,7 @@ namespace Discord.Audio { if (ConnectionState == ConnectionState.Connected) { - await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); return; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 4cf53d2a3..7231db649 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -35,8 +35,7 @@ namespace Discord public LoginState LoginState { get; private set; } /// Creates a new REST-only discord client. - public DiscordClient() - : this(new DiscordConfig()) { } + public DiscordClient() : this(new DiscordConfig()) { } /// Creates a new REST-only discord client. public DiscordClient(DiscordConfig config) { diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 7bb2e93a6..c1abfb6d3 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -35,6 +35,7 @@ namespace Discord private bool _isReconnecting; private int _unavailableGuilds; private long _lastGuildAvailableTime; + private int _nextAudioId; /// Gets the shard if of this client. public int ShardId { get; } @@ -74,6 +75,7 @@ namespace Discord LargeThreshold = config.LargeThreshold; AudioMode = config.AudioMode; WebSocketProvider = config.WebSocketProvider; + _nextAudioId = 1; _gatewayLogger = LogManager.CreateLogger("Gateway"); #if BENCHMARK @@ -87,7 +89,7 @@ namespace Discord e.ErrorContext.Handled = true; }; - ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {(GatewayOpCode)opCode}").ConfigureAwait(false); + ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); ApiClient.ReceivedGatewayEvent += ProcessMessageAsync; ApiClient.Disconnected += async ex => { @@ -1173,8 +1175,8 @@ namespace Discord var guild = DataStore.GetGuild(data.GuildId); if (guild != null) { - string endpoint = "wss://" + data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); - await guild.ConnectAudio(endpoint, data.Token).ConfigureAwait(false); + string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); + var _ = guild.ConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false); } else { diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index d75ba4bab..d4c5153b4 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -261,7 +261,7 @@ namespace Discord return null; } - public async Task ConnectAudio(string url, string token) + public async Task ConnectAudio(int id, string url, string token) { AudioClient audioClient; await _audioLock.WaitAsync().ConfigureAwait(false); @@ -271,7 +271,7 @@ namespace Discord audioClient = AudioClient; if (audioClient == null) { - audioClient = new AudioClient(this); + audioClient = new AudioClient(this, id); audioClient.Disconnected += async ex => { await _audioLock.WaitAsync().ConfigureAwait(false); diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 76133d6d7..ceac7be0a 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -26,6 +26,8 @@ "System.IO.Compression": "4.1.0", "System.IO.FileSystem": "4.0.1", "System.Net.Http": "4.1.0", + "System.Net.NameResolution": "4.0.0", + "System.Net.Sockets": "4.1.0", "System.Net.WebSockets.Client": "4.0.0", "System.Reflection.Extensions": "4.0.1", "System.Runtime.InteropServices": "4.1.0", From 142e8484eae658f219be5e6658a36685abb5316c Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 05:26:58 -0300 Subject: [PATCH 152/160] Added RTP Read/Write and Opus Encode/Decode streams --- src/Discord.Net/API/DiscordAPIClient.cs | 2 +- src/Discord.Net/API/DiscordVoiceAPIClient.cs | 2 +- src/Discord.Net/Audio/AudioClient.cs | 21 +++++- src/Discord.Net/Audio/IAudioClient.cs | 4 ++ src/Discord.Net/Audio/Opus/OpusApplication.cs | 4 +- src/Discord.Net/Audio/Opus/OpusConverter.cs | 14 +++- .../Audio/Opus/{Ctl.cs => OpusCtl.cs} | 4 +- src/Discord.Net/Audio/Opus/OpusDecoder.cs | 15 +++-- src/Discord.Net/Audio/Opus/OpusEncoder.cs | 53 +++++---------- src/Discord.Net/Audio/Opus/OpusError.cs | 2 +- src/Discord.Net/Audio/Sodium/SecretBox.cs | 16 +++-- .../Audio/Streams/OpusDecodeStream.cs | 30 +++++++++ .../Audio/Streams/OpusEncodeStream.cs | 34 ++++++++++ .../Audio/Streams/RTPReadStream.cs | 53 +++++++++++++++ .../Audio/Streams/RTPWriteStream.cs | 66 +++++++++++++++++++ 15 files changed, 257 insertions(+), 63 deletions(-) rename src/Discord.Net/Audio/Opus/{Ctl.cs => OpusCtl.cs} (72%) create mode 100644 src/Discord.Net/Audio/Streams/OpusDecodeStream.cs create mode 100644 src/Discord.Net/Audio/Streams/OpusEncodeStream.cs create mode 100644 src/Discord.Net/Audio/Streams/RTPReadStream.cs create mode 100644 src/Discord.Net/Audio/Streams/RTPWriteStream.cs diff --git a/src/Discord.Net/API/DiscordAPIClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs index a8644f97b..8bcd4b079 100644 --- a/src/Discord.Net/API/DiscordAPIClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -91,7 +91,7 @@ namespace Discord.API _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs index ecbd47778..2b278d903 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -83,7 +83,7 @@ namespace Discord.Audio _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index 210cce347..cc357a910 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -14,6 +14,8 @@ namespace Discord.Audio { internal class AudioClient : IAudioClient, IDisposable { + public const int SampleRate = 48000; + public event Func Connected { add { _connectedEvent.Add(value); } @@ -57,7 +59,7 @@ namespace Discord.Audio private DiscordSocketClient Discord => Guild.Discord; /// Creates a new REST/WebSocket discord client. - internal AudioClient(CachedGuild guild, int id) + public AudioClient(CachedGuild guild, int id) { Guild = guild; @@ -171,6 +173,22 @@ namespace Discord.Audio await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } + public void Send(byte[] data, int count) + { + //TODO: Queue these? + ApiClient.SendAsync(data, count).ConfigureAwait(false); + } + + public RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000) + { + return new RTPWriteStream(this, _secretKey, samplesPerFrame, _ssrc, bufferSize = 4000); + } + public OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, + OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) + { + return new OpusEncodeStream(this, _secretKey, samplesPerFrame, _ssrc, SampleRate, bitrate, channels, application, bufferSize); + } + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { #if BENCHMARK @@ -253,7 +271,6 @@ namespace Discord.Audio } #endif } - private async Task ProcessPacketAsync(byte[] packet) { if (!_connectTask.Task.IsCompleted) diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net/Audio/IAudioClient.cs index 40a75d4b5..b225312dc 100644 --- a/src/Discord.Net/Audio/IAudioClient.cs +++ b/src/Discord.Net/Audio/IAudioClient.cs @@ -16,5 +16,9 @@ namespace Discord.Audio int Latency { get; } Task DisconnectAsync(); + + RTPWriteStream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000); + OpusEncodeStream CreatePCMStream(int samplesPerFrame, int? bitrate = null, int channels = 2, + OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000); } } diff --git a/src/Discord.Net/Audio/Opus/OpusApplication.cs b/src/Discord.Net/Audio/Opus/OpusApplication.cs index cbaa894a5..d6a3ce0cf 100644 --- a/src/Discord.Net/Audio/Opus/OpusApplication.cs +++ b/src/Discord.Net/Audio/Opus/OpusApplication.cs @@ -1,6 +1,6 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { - internal enum OpusApplication : int + public enum OpusApplication : int { Voice = 2048, MusicOrMixed = 2049, diff --git a/src/Discord.Net/Audio/Opus/OpusConverter.cs b/src/Discord.Net/Audio/Opus/OpusConverter.cs index f430d07f7..732006990 100644 --- a/src/Discord.Net/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net/Audio/Opus/OpusConverter.cs @@ -1,6 +1,6 @@ using System; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal abstract class OpusConverter : IDisposable { @@ -8,17 +8,27 @@ namespace Discord.Audio.Opus /// Gets the bit rate of this converter. public const int BitsPerSample = 16; + /// Gets the bytes per sample. + public const int SampleSize = (BitsPerSample / 8) * MaxChannels; + /// Gets the maximum amount of channels this encoder supports. + public const int MaxChannels = 2; + /// Gets the input sampling rate of this converter. public int SamplingRate { get; } + /// Gets the number of samples per second for this stream. + public int Channels { get; } - protected OpusConverter(int samplingRate) + protected OpusConverter(int samplingRate, int channels) { if (samplingRate != 8000 && samplingRate != 12000 && samplingRate != 16000 && samplingRate != 24000 && samplingRate != 48000) throw new ArgumentOutOfRangeException(nameof(samplingRate)); + if (channels != 1 && channels != 2) + throw new ArgumentOutOfRangeException(nameof(channels)); SamplingRate = samplingRate; + Channels = channels; } private bool disposedValue = false; // To detect redundant calls diff --git a/src/Discord.Net/Audio/Opus/Ctl.cs b/src/Discord.Net/Audio/Opus/OpusCtl.cs similarity index 72% rename from src/Discord.Net/Audio/Opus/Ctl.cs rename to src/Discord.Net/Audio/Opus/OpusCtl.cs index 5023782da..e71213ae6 100644 --- a/src/Discord.Net/Audio/Opus/Ctl.cs +++ b/src/Discord.Net/Audio/Opus/OpusCtl.cs @@ -1,6 +1,6 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { - internal enum Ctl : int + internal enum OpusCtl : int { SetBitrateRequest = 4002, GetBitrateRequest = 4003, diff --git a/src/Discord.Net/Audio/Opus/OpusDecoder.cs b/src/Discord.Net/Audio/Opus/OpusDecoder.cs index 2df7c2414..e3a3fa649 100644 --- a/src/Discord.Net/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net/Audio/Opus/OpusDecoder.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal unsafe class OpusDecoder : OpusConverter { @@ -10,13 +10,13 @@ namespace Discord.Audio.Opus [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern void DestroyDecoder(IntPtr decoder); [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] - private static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); + private static extern int Decode(IntPtr st, byte* data, int len, byte* pcm, int max_frame_size, int decode_fec); - public OpusDecoder(int samplingRate) - : base(samplingRate) + public OpusDecoder(int samplingRate, int channels) + : base(samplingRate, channels) { OpusError error; - _ptr = CreateDecoder(samplingRate, 2, out error); + _ptr = CreateDecoder(samplingRate, channels, out error); if (error != OpusError.OK) throw new InvalidOperationException($"Error occured while creating decoder: {error}"); } @@ -25,11 +25,12 @@ namespace Discord.Audio.Opus /// PCM samples to decode. /// Offset of the frame in input. /// Buffer to store the decoded frame. - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output) + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) - result = Decode(_ptr, inPtr + inputOffset, inputCount, output, inputCount, 0); + fixed (byte* outPtr = output) + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0); if (result < 0) throw new Exception(((OpusError)result).ToString()); diff --git a/src/Discord.Net/Audio/Opus/OpusEncoder.cs b/src/Discord.Net/Audio/Opus/OpusEncoder.cs index e17487f43..145447194 100644 --- a/src/Discord.Net/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net/Audio/Opus/OpusEncoder.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Discord.Audio.Opus +namespace Discord.Audio { internal unsafe class OpusEncoder : OpusConverter { @@ -10,49 +10,22 @@ namespace Discord.Audio.Opus [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] private static extern void DestroyEncoder(IntPtr encoder); [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] - private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); + private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - private static extern int EncoderCtl(IntPtr st, Ctl request, int value); - - /// Gets the bit rate in kbit/s. - public int? BitRate { get; } + private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); + /// Gets the coding mode of the encoder. public OpusApplication Application { get; } - /// Gets the number of channels of this converter. - public int InputChannels { get; } - /// Gets the milliseconds per frame. - public int FrameMilliseconds { get; } - - /// Gets the bytes per sample. - public int SampleSize => (BitsPerSample / 8) * InputChannels; - /// Gets the number of samples per frame. - public int SamplesPerFrame => SamplingRate / 1000 * FrameMilliseconds; - /// Gets the bytes per frame. - public int FrameSize => SamplesPerFrame * SampleSize; - public OpusEncoder(int samplingRate, int channels, int frameMillis, - int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed) - : base(samplingRate) + public OpusEncoder(int samplingRate, int channels, OpusApplication application = OpusApplication.MusicOrMixed) + : base(samplingRate, channels) { - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - if (bitrate != null && (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)) - throw new ArgumentOutOfRangeException(nameof(bitrate)); + Application = application; OpusError error; _ptr = CreateEncoder(samplingRate, channels, (int)application, out error); if (error != OpusError.OK) throw new InvalidOperationException($"Error occured while creating encoder: {error}"); - - - BitRate = bitrate; - Application = application; - InputChannels = channels; - FrameMilliseconds = frameMillis; - - SetForwardErrorCorrection(true); - if (bitrate != null) - SetBitrate(bitrate.Value); } @@ -61,11 +34,12 @@ namespace Discord.Audio.Opus /// Offset of the frame in pcmSamples. /// Buffer to store the encoded frame. /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) + public unsafe int EncodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) - result = Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); + fixed (byte* outPtr = output) + result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset); if (result < 0) throw new Exception(((OpusError)result).ToString()); @@ -75,7 +49,7 @@ namespace Discord.Audio.Opus /// Gets or sets whether Forward Error Correction is enabled. public void SetForwardErrorCorrection(bool value) { - var result = EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0); + var result = EncoderCtl(_ptr, OpusCtl.SetInbandFECRequest, value ? 1 : 0); if (result < 0) throw new Exception(((OpusError)result).ToString()); } @@ -83,7 +57,10 @@ namespace Discord.Audio.Opus /// Gets or sets whether Forward Error Correction is enabled. public void SetBitrate(int value) { - var result = EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000); + if (value < 1 || value > DiscordVoiceAPIClient.MaxBitrate) + throw new ArgumentOutOfRangeException(nameof(value)); + + var result = EncoderCtl(_ptr, OpusCtl.SetBitrateRequest, value * 1000); if (result < 0) throw new Exception(((OpusError)result).ToString()); } diff --git a/src/Discord.Net/Audio/Opus/OpusError.cs b/src/Discord.Net/Audio/Opus/OpusError.cs index 5bfb92d98..d29d8b9dd 100644 --- a/src/Discord.Net/Audio/Opus/OpusError.cs +++ b/src/Discord.Net/Audio/Opus/OpusError.cs @@ -1,4 +1,4 @@ -namespace Discord.Audio.Opus +namespace Discord.Audio { internal enum OpusError : int { diff --git a/src/Discord.Net/Audio/Sodium/SecretBox.cs b/src/Discord.Net/Audio/Sodium/SecretBox.cs index 727db2711..ba4bc2e62 100644 --- a/src/Discord.Net/Audio/Sodium/SecretBox.cs +++ b/src/Discord.Net/Audio/Sodium/SecretBox.cs @@ -1,23 +1,25 @@ using System.Runtime.InteropServices; -namespace Discord.Net.Audio.Sodium +namespace Discord.Audio { public unsafe static class SecretBox { [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] - private static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); + private static extern int SecretBoxOpenEasy(byte* output, byte* input, long inputLength, byte[] nonce, byte[] secret); - public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) + public static int Encrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) { + fixed (byte* inPtr = input) fixed (byte* outPtr = output) - return SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); + return SecretBoxEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); } - public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) + public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) { fixed (byte* inPtr = input) - return SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); + fixed (byte* outPtr = output) + return SecretBoxOpenEasy(outPtr + outputOffset, inPtr + inputOffset, inputLength, nonce, secret); } } } diff --git a/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs new file mode 100644 index 000000000..c059955a8 --- /dev/null +++ b/src/Discord.Net/Audio/Streams/OpusDecodeStream.cs @@ -0,0 +1,30 @@ +namespace Discord.Audio +{ + public class OpusDecodeStream : RTPReadStream + { + private readonly byte[] _buffer; + private readonly OpusDecoder _decoder; + + internal OpusDecodeStream(AudioClient audioClient, byte[] secretKey, int samplingRate, + int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + : base(audioClient, secretKey) + { + _buffer = new byte[bufferSize]; + _decoder = new OpusDecoder(samplingRate, channels); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); + return base.Read(_buffer, 0, count); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _decoder.Dispose(); + } + } +} diff --git a/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs new file mode 100644 index 000000000..deb44f619 --- /dev/null +++ b/src/Discord.Net/Audio/Streams/OpusEncodeStream.cs @@ -0,0 +1,34 @@ +namespace Discord.Audio +{ + public class OpusEncodeStream : RTPWriteStream + { + private readonly byte[] _buffer; + private readonly OpusEncoder _encoder; + + internal OpusEncodeStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int samplingRate, int? bitrate = null, + int channels = OpusConverter.MaxChannels, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000) + : base(audioClient, secretKey, samplesPerFrame, ssrc) + { + _buffer = new byte[bufferSize]; + _encoder = new OpusEncoder(samplingRate, channels); + + _encoder.SetForwardErrorCorrection(true); + if (bitrate != null) + _encoder.SetBitrate(bitrate.Value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + count = _encoder.EncodeFrame(buffer, offset, count, _buffer, 0); + base.Write(_buffer, 0, count); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + _encoder.Dispose(); + } + } +} diff --git a/src/Discord.Net/Audio/Streams/RTPReadStream.cs b/src/Discord.Net/Audio/Streams/RTPReadStream.cs new file mode 100644 index 000000000..4bf7f5e1b --- /dev/null +++ b/src/Discord.Net/Audio/Streams/RTPReadStream.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.IO; + +namespace Discord.Audio +{ + public class RTPReadStream : Stream + { + private readonly BlockingCollection _queuedData; //TODO: Replace with max-length ring buffer + private readonly AudioClient _audioClient; + private readonly byte[] _buffer, _nonce, _secretKey; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + internal RTPReadStream(AudioClient audioClient, byte[] secretKey, int bufferSize = 4000) + { + _audioClient = audioClient; + _secretKey = secretKey; + _buffer = new byte[bufferSize]; + _queuedData = new BlockingCollection(100); + _nonce = new byte[24]; + } + + public override int Read(byte[] buffer, int offset, int count) + { + var queuedData = _queuedData.Take(); + Buffer.BlockCopy(queuedData, 0, buffer, offset, Math.Min(queuedData.Length, count)); + return queuedData.Length; + } + public override void Write(byte[] buffer, int offset, int count) + { + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); + count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); + var newBuffer = new byte[count]; + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); + _queuedData.Add(newBuffer); + } + + public override void Flush() { throw new NotSupportedException(); } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net/Audio/Streams/RTPWriteStream.cs new file mode 100644 index 000000000..f871ded0d --- /dev/null +++ b/src/Discord.Net/Audio/Streams/RTPWriteStream.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; + +namespace Discord.Audio +{ + public class RTPWriteStream : Stream + { + private readonly AudioClient _audioClient; + private readonly byte[] _buffer, _nonce, _secretKey; + private int _samplesPerFrame; + private uint _ssrc, _timestamp = 0; + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + + + internal RTPWriteStream(AudioClient audioClient, byte[] secretKey, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + { + _audioClient = audioClient; + _secretKey = secretKey; + _samplesPerFrame = samplesPerFrame; + _ssrc = ssrc; + _nonce = new byte[24]; + _buffer = new byte[bufferSize]; + _buffer[0] = 0x80; + _buffer[1] = 0x78; + _buffer[8] = (byte)(_ssrc >> 24); + _buffer[9] = (byte)(_ssrc >> 16); + _buffer[10] = (byte)(_ssrc >> 8); + _buffer[11] = (byte)(_ssrc >> 0); + } + + public override void Write(byte[] buffer, int offset, int count) + { + unchecked + { + if (_buffer[3]++ == byte.MaxValue) + _buffer[4]++; + + _timestamp += (uint)_samplesPerFrame; + _buffer[4] = (byte)(_timestamp >> 24); + _buffer[5] = (byte)(_timestamp >> 16); + _buffer[6] = (byte)(_timestamp >> 8); + _buffer[7] = (byte)(_timestamp >> 0); + } + + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); + count = SecretBox.Encrypt(buffer, offset, count, _buffer, 12, _nonce, _secretKey); + _audioClient.Send(_buffer, count); + } + + public override void Flush() { } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +} From 5c5a9c8d6f4a184dad964415eb227aff40c56214 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 05:27:18 -0300 Subject: [PATCH 153/160] Fixed User TypeReader not resolving in DMs --- .../Readers/UserTypeReader.cs | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index c80ac2816..e4bd9ffd1 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -9,48 +9,44 @@ namespace Discord.Commands { public override async Task Read(IMessage context, string input) { - IGuildChannel guildChannel = context.Channel as IGuildChannel; IUser result = null; - - if (guildChannel != null) + + //By Id + ulong id; + if (MentionUtils.TryParseUser(input, out id) || ulong.TryParse(input, out id)) { - //By Id - ulong id; - if (MentionUtils.TryParseUser(input, out id) || ulong.TryParse(input, out id)) - { - var user = await guildChannel.Guild.GetUserAsync(id).ConfigureAwait(false); - if (user != null) - result = user; - } + var user = await context.Channel.GetUserAsync(id).ConfigureAwait(false); + if (user != null) + result = user; + } - //By Username + Discriminator - if (result == null) + //By Username + Discriminator + if (result == null) + { + int index = input.LastIndexOf('#'); + if (index >= 0) { - int index = input.LastIndexOf('#'); - if (index >= 0) + string username = input.Substring(0, index); + ushort discriminator; + if (ushort.TryParse(input.Substring(index + 1), out discriminator)) { - string username = input.Substring(0, index); - ushort discriminator; - if (ushort.TryParse(input.Substring(index + 1), out discriminator)) - { - var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); - result = users.Where(x => - x.DiscriminatorValue == discriminator && - string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); - } + var users = await context.Channel.GetUsersAsync().ConfigureAwait(false); + result = users.Where(x => + x.DiscriminatorValue == discriminator && + string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); } } + } - //By Username - if (result == null) - { - var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); - var filteredUsers = users.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)).ToArray(); - if (filteredUsers.Length > 1) - return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple users found."); - else if (filteredUsers.Length == 1) - result = filteredUsers[0]; - } + //By Username + if (result == null) + { + var users = await context.Channel.GetUsersAsync().ConfigureAwait(false); + var filteredUsers = users.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (filteredUsers.Length > 1) + return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple users found."); + else if (filteredUsers.Length == 1) + result = filteredUsers[0]; } if (result == null) From 37ab13160b1caf6cfde5ad01eb52de56e9b9c001 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 05:27:29 -0300 Subject: [PATCH 154/160] Fixed CommandService.Execute crash bug --- src/Discord.Net.Commands/CommandService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f3b623700..a9e7085e0 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -281,7 +281,7 @@ namespace Discord.Commands return searchResult; var commands = searchResult.Commands; - for (int i = commands.Count - 1; i >= 0; i++) + for (int i = commands.Count - 1; i >= 0; i--) { var parseResult = await commands[i].Parse(message, searchResult); if (!parseResult.IsSuccess) From c0a60784525bdc16c2debdfe7c898212ba056659 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 05:41:06 -0300 Subject: [PATCH 155/160] Removed Discord.Net.Audio reference --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4eb5dd487..b61ad5cf2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.htm You can download Discord.Net and its extensions from NuGet: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) -- [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) ### Compiling In order to compile Discord.Net, you require the following: From e4762b1522f8ad4553b60cd0fe6df51b9fa28d5d Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 05:41:42 -0300 Subject: [PATCH 156/160] extensions -> command extension --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b61ad5cf2..d67ac430b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). ### Installation -You can download Discord.Net and its extensions from NuGet: +You can download Discord.Net and its command extension from NuGet: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) From 7723130713a2fa88c9098adf7330bd145968d294 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 29 Jun 2016 07:03:44 -0300 Subject: [PATCH 157/160] Implemented new CommandMap --- src/Discord.Net.Commands/CommandService.cs | 50 ++-------- src/Discord.Net.Commands/Map/CommandMap.cs | 76 +++++++++++++++ .../Map/CommandMapNode.cs | 95 +++++++++++++++++++ 3 files changed, 178 insertions(+), 43 deletions(-) create mode 100644 src/Discord.Net.Commands/Map/CommandMap.cs create mode 100644 src/Discord.Net.Commands/Map/CommandMapNode.cs diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index a9e7085e0..794ab0863 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -14,8 +14,8 @@ namespace Discord.Commands { private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _modules; - private readonly ConcurrentDictionary> _map; private readonly ConcurrentDictionary _typeReaders; + private readonly CommandMap _map; public IEnumerable Modules => _modules.Select(x => x.Value); public IEnumerable Commands => _modules.SelectMany(x => x.Value.Commands); @@ -24,7 +24,7 @@ namespace Discord.Commands { _moduleLock = new SemaphoreSlim(1, 1); _modules = new ConcurrentDictionary(); - _map = new ConcurrentDictionary>(); + _map = new CommandMap(); _typeReaders = new ConcurrentDictionary { [typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), @@ -160,11 +160,7 @@ namespace Discord.Commands _modules[moduleInstance] = loadedModule; foreach (var cmd in loadedModule.Commands) - { - var list = _map.GetOrAdd(cmd.Text, _ => new List()); - lock (list) - list.Add(cmd); - } + _map.AddCommand(cmd); return loadedModule; } @@ -222,14 +218,7 @@ namespace Discord.Commands if (_modules.TryRemove(module, out unloadedModule)) { foreach (var cmd in unloadedModule.Commands) - { - List list; - if (_map.TryGetValue(cmd.Text, out list)) - { - lock (list) - list.Remove(cmd); - } - } + _map.RemoveCommand(cmd); return true; } else @@ -240,35 +229,10 @@ namespace Discord.Commands public SearchResult Search(IMessage message, string input) { string lowerInput = input.ToLowerInvariant(); - - ImmutableArray.Builder matches = null; - List group; - int pos = -1; - - while (true) - { - pos = input.IndexOf(' ', pos + 1); - string cmdText = pos == -1 ? input : input.Substring(0, pos); - if (!_map.TryGetValue(cmdText, out group)) - break; - - lock (group) - { - if (matches == null) - matches = ImmutableArray.CreateBuilder(group.Count); - for (int i = 0; i < group.Count; i++) - matches.Add(group[i]); - } - - if (pos == -1) - { - pos = input.Length; - break; - } - } + var matches = _map.GetCommands(input).ToImmutableArray(); - if (matches != null) - return SearchResult.FromSuccess(input, matches.ToImmutable()); + if (matches.Length > 0) + return SearchResult.FromSuccess(input, matches); else return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } diff --git a/src/Discord.Net.Commands/Map/CommandMap.cs b/src/Discord.Net.Commands/Map/CommandMap.cs new file mode 100644 index 000000000..0f719d56d --- /dev/null +++ b/src/Discord.Net.Commands/Map/CommandMap.cs @@ -0,0 +1,76 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Commands +{ + internal class CommandMap + { + private readonly ConcurrentDictionary _nodes; + + public CommandMap() + { + _nodes = new ConcurrentDictionary(); + } + + public void AddCommand(Command command) + { + string text = command.Text; + int nextSpace = text.IndexOf(' '); + string name; + + lock (this) + { + if (nextSpace == -1) + name = command.Text; + else + name = command.Text.Substring(0, nextSpace); + var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); + nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + } + } + public void RemoveCommand(Command command) + { + string text = command.Text; + int nextSpace = text.IndexOf(' '); + string name; + + lock (this) + { + if (nextSpace == -1) + name = command.Text; + else + name = command.Text.Substring(0, nextSpace); + + CommandMapNode nextNode; + if (_nodes.TryGetValue(name, out nextNode)) + { + nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + if (nextNode.IsEmpty) + _nodes.TryRemove(name, out nextNode); + } + } + } + + public IEnumerable GetCommands(string text) + { + int nextSpace = text.IndexOf(' '); + string name; + + lock (this) + { + if (nextSpace == -1) + name = text; + else + name = text.Substring(0, nextSpace); + + CommandMapNode nextNode; + if (_nodes.TryGetValue(name, out nextNode)) + return nextNode.GetCommands(text, nextSpace + 1); + else + return Enumerable.Empty(); + } + } + } +} diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs new file mode 100644 index 000000000..1ce0b4724 --- /dev/null +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -0,0 +1,95 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord.Commands +{ + internal class CommandMapNode + { + private readonly ConcurrentDictionary _nodes; + private readonly string _name; + private ImmutableArray _commands; + + public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; + + public CommandMapNode(string name) + { + _name = name; + _nodes = new ConcurrentDictionary(); + _commands = ImmutableArray.Create(); + } + + public void AddCommand(string text, int index, Command command) + { + int nextSpace = text.IndexOf(' ', index); + string name; + + lock (this) + { + if (text == "") + _commands = _commands.Add(command); + else + { + if (nextSpace == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSpace - index); + + var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); + nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + } + } + } + public void RemoveCommand(string text, int index, Command command) + { + int nextSpace = text.IndexOf(' ', index); + string name; + + lock (this) + { + if (text == "") + _commands = _commands.Remove(command); + else + { + if (nextSpace == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSpace - index); + + CommandMapNode nextNode; + if (_nodes.TryGetValue(name, out nextNode)) + { + nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); + if (nextNode.IsEmpty) + _nodes.TryRemove(name, out nextNode); + } + } + } + } + + public IEnumerable GetCommands(string text, int index) + { + int nextSpace = text.IndexOf(' ', index); + string name; + + var commands = _commands; + for (int i = 0; i < commands.Length; i++) + yield return _commands[i]; + + if (text != "") + { + if (nextSpace == -1) + name = text.Substring(index); + else + name = text.Substring(index, nextSpace - index); + + CommandMapNode nextNode; + if (_nodes.TryGetValue(name, out nextNode)) + { + foreach (var cmd in nextNode.GetCommands(nextSpace == -1 ? "" : text, nextSpace + 1)) + yield return cmd; + } + } + } + } +} From 2dea7ff7a9e4ee4872a4d8187732d45f82fc3f29 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 7 Jul 2016 11:30:36 -0300 Subject: [PATCH 158/160] Added IAttachment and several missing properties. --- src/Discord.Net/API/Common/EmbedThumbnail.cs | 4 ++-- src/Discord.Net/Entities/Messages/Attachment.cs | 14 +++++++++++--- .../Entities/Messages/EmbedThumbnail.cs | 8 +++++++- src/Discord.Net/Entities/Messages/IAttachment.cs | 14 ++++++++++++++ src/Discord.Net/Entities/Messages/IMessage.cs | 2 +- src/Discord.Net/Entities/Messages/Message.cs | 2 +- 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/Discord.Net/Entities/Messages/IAttachment.cs diff --git a/src/Discord.Net/API/Common/EmbedThumbnail.cs b/src/Discord.Net/API/Common/EmbedThumbnail.cs index 9933183af..8534d427a 100644 --- a/src/Discord.Net/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net/API/Common/EmbedThumbnail.cs @@ -9,8 +9,8 @@ namespace Discord.API [JsonProperty("proxy_url")] public string ProxyUrl { get; set; } [JsonProperty("height")] - public int Height { get; set; } + public Optional Height { get; set; } [JsonProperty("width")] - public int Width { get; set; } + public Optional Width { get; set; } } } diff --git a/src/Discord.Net/Entities/Messages/Attachment.cs b/src/Discord.Net/Entities/Messages/Attachment.cs index 5f2c1ed47..1cc24749e 100644 --- a/src/Discord.Net/Entities/Messages/Attachment.cs +++ b/src/Discord.Net/Entities/Messages/Attachment.cs @@ -2,17 +2,25 @@ namespace Discord { - public struct Attachment + internal class Attachment : IAttachment { public ulong Id { get; } - public int Size { get; } public string Filename { get; } + public string Url { get; } + public string ProxyUrl { get; } + public int Size { get; } + public int? Height { get; } + public int? Width { get; } public Attachment(Model model) { Id = model.Id; - Size = model.Size; Filename = model.Filename; + Size = model.Size; + Url = model.Url; + ProxyUrl = model.ProxyUrl; + Height = model.Height.IsSpecified ? model.Height.Value : (int?)null; + Width = model.Width.IsSpecified ? model.Width.Value : (int?)null; } } } diff --git a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs index 8630dc473..736d7d743 100644 --- a/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs @@ -18,6 +18,12 @@ namespace Discord } internal EmbedThumbnail(Model model) - : this(model.Url, model.ProxyUrl, model.Height, model.Width) { } + : this( + model.Url, + model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null) + { + } } } diff --git a/src/Discord.Net/Entities/Messages/IAttachment.cs b/src/Discord.Net/Entities/Messages/IAttachment.cs new file mode 100644 index 000000000..225e9cf2e --- /dev/null +++ b/src/Discord.Net/Entities/Messages/IAttachment.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public interface IAttachment + { + ulong Id { get; } + + string Filename { get; } + string Url { get; } + string ProxyUrl { get; } + int Size { get; } + int? Height { get; } + int? Width { get; } + } +} diff --git a/src/Discord.Net/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs index 61f1199eb..e33670acb 100644 --- a/src/Discord.Net/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -25,7 +25,7 @@ namespace Discord /// Gets the author of this message. IUser Author { get; } /// Returns a collection of all attachments included in this message. - IReadOnlyCollection Attachments { get; } + IReadOnlyCollection Attachments { get; } /// Returns a collection of all embeds included in this message. IReadOnlyCollection Embeds { get; } /// Returns a collection of channel ids mentioned in this message. diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index e0fba96cb..1ac7a14f3 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -23,7 +23,7 @@ namespace Discord public IMessageChannel Channel { get; } public IUser Author { get; } - public IReadOnlyCollection Attachments { get; private set; } + public IReadOnlyCollection Attachments { get; private set; } public IReadOnlyCollection Embeds { get; private set; } public IReadOnlyCollection MentionedChannelIds { get; private set; } public IReadOnlyCollection MentionedRoles { get; private set; } From 3ad3912606337d28ef25ba629299929598833960 Mon Sep 17 00:00:00 2001 From: Khionu Terabite Date: Thu, 7 Jul 2016 20:35:46 -0400 Subject: [PATCH 159/160] We all do it now and then <3 --- src/Discord.Net.Commands/CommandService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 794ab0863..a3400a465 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -144,7 +144,7 @@ namespace Discord.Commands var typeInfo = moduleInstance.GetType().GetTypeInfo(); var moduleAttr = typeInfo.GetCustomAttribute(); - if (moduleAttr != null) + if (moduleAttr == null) throw new ArgumentException($"Modules must be marked with ModuleAttribute."); return LoadInternal(moduleInstance, moduleAttr, typeInfo); From 5488294e0f108dfaf941f7a1c2c231b40907fcdf Mon Sep 17 00:00:00 2001 From: Khionu Terabite Date: Thu, 7 Jul 2016 23:09:05 -0400 Subject: [PATCH 160/160] Fixed Pinning on Modify --- src/Discord.Net/Entities/Messages/Message.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Discord.Net/Entities/Messages/Message.cs b/src/Discord.Net/Entities/Messages/Message.cs index 1ac7a14f3..225890578 100644 --- a/src/Discord.Net/Entities/Messages/Message.cs +++ b/src/Discord.Net/Entities/Messages/Message.cs @@ -157,9 +157,7 @@ namespace Discord model = await Discord.ApiClient.ModifyMessageAsync(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else model = await Discord.ApiClient.ModifyDMMessageAsync(Channel.Id, Id, args).ConfigureAwait(false); - { - await Discord.ApiClient.AddPinAsync(Channel.Id, Id).ConfigureAwait(false); - } + Update(model, UpdateSource.Rest); } public async Task DeleteAsync()