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)); - } - } - } -}