diff --git a/README.md b/README.md index a48876c5c..06f3ae426 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # Discord.Net v1.0.0-dev -[![NuGet Pre Release](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![AppVeyor](https://img.shields.io/appveyor/ci/foxbot/discord-net.svg?maxAge=2592000?style=plastic)](https://ci.appveyor.com/project/foxbot/discord-net/) [![Discord](https://discordapp.com/api/servers/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW) +[![Build status](https://ci.appveyor.com/api/projects/status/p0n69xhqgmoobycf/branch/master?svg=true)](https://ci.appveyor.com/project/foxbot/discord-net/branch/master) -Discord.Net is an API wrapper for [Discord](http://discordapp.com) written in C#. +An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). -Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). +Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). -## Installing +##### 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/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) @@ -14,9 +17,8 @@ You can download Discord.Net and its extensions from NuGet: ### Compiling In order to compile Discord.Net, you require at least the following: -- [Visual Studio 15](https://www.visualstudio.com/downloads/download-visual-studio-vs) -- [ASP.Net 5 RC1](https://get.asp.net) -- NuGet 3.3 (available through Visual Studio) +- [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) +- NuGet 3.3+ (available through Visual Studio) -### Known Issues -- .Net Core support is incomplete on non-Windows systems diff --git a/global.json b/global.json new file mode 100644 index 000000000..7ee23dc6a --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-preview1-002702" + } +} diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs index 6dda88944..f50c31a09 100644 --- a/src/Discord.Net/API/Common/Channel.cs +++ b/src/Discord.Net/API/Common/Channel.cs @@ -10,7 +10,7 @@ namespace Discord.API [JsonProperty("is_private")] public bool IsPrivate { get; set; } [JsonProperty("last_message_id")] - public ulong LastMessageId { get; set; } + public ulong? LastMessageId { get; set; } //GuildChannel [JsonProperty("guild_id")] @@ -23,10 +23,16 @@ namespace Discord.API public int Position { get; set; } [JsonProperty("permission_overwrites")] public Overwrite[] PermissionOverwrites { get; set; } + + //TextChannel [JsonProperty("topic")] public string Topic { get; set; } + + //VoiceChannel [JsonProperty("bitrate")] public int Bitrate { get; set; } + [JsonProperty("user_limit")] + public int UserLimit { get; set; } //DMChannel [JsonProperty("recipient")] diff --git a/src/Discord.Net/API/Common/Game.cs b/src/Discord.Net/API/Common/Game.cs new file mode 100644 index 000000000..a5bbbfcdc --- /dev/null +++ b/src/Discord.Net/API/Common/Game.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Game + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string StreamUrl { get; set; } + [JsonProperty("type")] + public StreamType StreamType { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Overwrite.cs b/src/Discord.Net/API/Common/Overwrite.cs index f1da83b9e..0d041e163 100644 --- a/src/Discord.Net/API/Common/Overwrite.cs +++ b/src/Discord.Net/API/Common/Overwrite.cs @@ -8,9 +8,9 @@ namespace Discord.API public ulong TargetId { get; set; } [JsonProperty("type")] public PermissionTarget TargetType { get; set; } - [JsonProperty("deny")] - public uint Deny { get; set; } - [JsonProperty("allow")] - public uint Allow { get; set; } + [JsonProperty("deny"), Int53] + public ulong Deny { get; set; } + [JsonProperty("allow"), Int53] + public ulong Allow { get; set; } } } diff --git a/src/Discord.Net/API/Common/ReadState.cs b/src/Discord.Net/API/Common/ReadState.cs index 6fa0c9b6e..e4177bedf 100644 --- a/src/Discord.Net/API/Common/ReadState.cs +++ b/src/Discord.Net/API/Common/ReadState.cs @@ -9,6 +9,6 @@ namespace Discord.API [JsonProperty("mention_count")] public int MentionCount { get; set; } [JsonProperty("last_message_id")] - public ulong LastMentionId { get; set; } + public ulong? LastMessageId { get; set; } } } diff --git a/src/Discord.Net/API/Common/Role.cs b/src/Discord.Net/API/Common/Role.cs index bd447fcd1..721b2a50b 100644 --- a/src/Discord.Net/API/Common/Role.cs +++ b/src/Discord.Net/API/Common/Role.cs @@ -14,8 +14,8 @@ namespace Discord.API public bool? Hoist { get; set; } [JsonProperty("position")] public int? Position { get; set; } - [JsonProperty("permissions")] - public uint? Permissions { get; set; } + [JsonProperty("permissions"), Int53] + public ulong? Permissions { get; set; } [JsonProperty("managed")] public bool? Managed { get; set; } } diff --git a/src/Discord.Net/API/Common/UserGuild.cs b/src/Discord.Net/API/Common/UserGuild.cs index 124f64688..7eaefca39 100644 --- a/src/Discord.Net/API/Common/UserGuild.cs +++ b/src/Discord.Net/API/Common/UserGuild.cs @@ -12,7 +12,7 @@ namespace Discord.API public string Icon { get; set; } [JsonProperty("owner")] public bool Owner { get; set; } - [JsonProperty("permissions")] - public uint Permissions { get; set; } + [JsonProperty("permissions"), Int53] + public ulong Permissions { get; set; } } } diff --git a/src/Discord.Net/API/DiscordRawClient.cs b/src/Discord.Net/API/DiscordAPIClient.cs similarity index 50% rename from src/Discord.Net/API/DiscordRawClient.cs rename to src/Discord.Net/API/DiscordAPIClient.cs index 60b571f74..dee9fddf0 100644 --- a/src/Discord.Net/API/DiscordRawClient.cs +++ b/src/Discord.Net/API/DiscordAPIClient.cs @@ -1,14 +1,19 @@ -using Discord.API.Rest; +using Discord.API.Gateway; +using Discord.API.Rest; using Discord.Net; using Discord.Net.Converters; +using Discord.Net.Queue; using Discord.Net.Rest; +using Discord.Net.WebSockets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; using System.Text; @@ -17,48 +22,118 @@ using System.Threading.Tasks; namespace Discord.API { - public class DiscordRawClient + public class DiscordApiClient : IDisposable { - internal event EventHandler SentRequest; + public event Func SentRequest; + public event Func SentGatewayMessage; + public event Func ReceivedGatewayEvent; private readonly RequestQueue _requestQueue; - private readonly IRestClient _restClient; - private readonly CancellationToken _cancelToken; private readonly JsonSerializer _serializer; - + private readonly IRestClient _restClient; + private readonly IWebSocketClient _gatewayClient; + private readonly SemaphoreSlim _connectionLock; + private CancellationTokenSource _loginCancelToken, _connectCancelToken; + private string _authToken; + private bool _isDisposed; + + public LoginState LoginState { get; private set; } + public ConnectionState ConnectionState { get; private set; } public TokenType AuthTokenType { get; private set; } - public IRestClient RestClient { get; private set; } - public IRequestQueue RequestQueue { get; private set; } - - internal DiscordRawClient(RestClientProvider restClientProvider, CancellationToken cancelToken) + + public DiscordApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider = null, JsonSerializer serializer = null, RequestQueue requestQueue = null) { - _cancelToken = cancelToken; + _connectionLock = new SemaphoreSlim(1, 1); + + _requestQueue = requestQueue ?? new RequestQueue(); - _restClient = restClientProvider(DiscordConfig.ClientAPIUrl, cancelToken); + _restClient = restClientProvider(DiscordConfig.ClientAPIUrl); _restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("user-agent", DiscordConfig.UserAgent); - _requestQueue = new RequestQueue(_restClient); + if (webSocketProvider != null) + { + _gatewayClient = webSocketProvider(); + _gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); + _gatewayClient.BinaryMessage += async (data, index, count) => + { + using (var compressed = new MemoryStream(data, index + 2, count - 2)) + using (var decompressed = new MemoryStream()) + { + using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) + zlib.CopyTo(decompressed); + decompressed.Position = 0; + using (var reader = new StreamReader(decompressed)) + { + var msg = JsonConvert.DeserializeObject(reader.ReadToEnd()); + await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + } + } + }; + _gatewayClient.TextMessage += async text => + { + var msg = JsonConvert.DeserializeObject(text); + await ReceivedGatewayEvent.Raise((GatewayOpCodes)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); + }; + } - _serializer = new JsonSerializer(); - _serializer.Converters.Add(new OptionalConverter()); - _serializer.Converters.Add(new ChannelTypeConverter()); - _serializer.Converters.Add(new ImageConverter()); - _serializer.Converters.Add(new NullableUInt64Converter()); - _serializer.Converters.Add(new PermissionTargetConverter()); - _serializer.Converters.Add(new StringEntityConverter()); - _serializer.Converters.Add(new UInt64ArrayConverter()); - _serializer.Converters.Add(new UInt64Converter()); - _serializer.Converters.Add(new UInt64EntityConverter()); - _serializer.Converters.Add(new UserStatusConverter()); - _serializer.ContractResolver = new OptionalContractResolver(); + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } - - public void SetToken(TokenType tokenType, string token) + void Dispose(bool disposing) { - AuthTokenType = tokenType; + if (!_isDisposed) + { + if (disposing) + { + _loginCancelToken?.Dispose(); + _connectCancelToken?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); - if (token != null) + public async Task Login(LoginParams args) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternal(TokenType.User, null, args, true).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + public async Task Login(TokenType tokenType, string token) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LoginInternal(tokenType, token, null, false).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LoginInternal(TokenType tokenType, string token, LoginParams args, bool doLogin) + { + if (LoginState != LoginState.LoggedOut) + await LogoutInternal().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + + try { + _loginCancelToken = new CancellationTokenSource(); + + AuthTokenType = TokenType.User; + _authToken = null; + _restClient.SetHeader("authorization", null); + await _requestQueue.SetCancelToken(_loginCancelToken.Token).ConfigureAwait(false); + _restClient.SetCancelToken(_loginCancelToken.Token); + + if (doLogin) + { + var response = await Send("POST", "auth/login", args, GlobalBucket.Login).ConfigureAwait(false); + token = response.Token; + } + + AuthTokenType = tokenType; + _authToken = token; switch (tokenType) { case TokenType.Bot: @@ -72,9 +147,105 @@ namespace Discord.API default: throw new ArgumentException("Unknown oauth token type", nameof(tokenType)); } + _restClient.SetHeader("authorization", token); + + LoginState = LoginState.LoggedIn; + } + catch (Exception) + { + await LogoutInternal().ConfigureAwait(false); + throw; + } + } + + public async Task Logout() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await LogoutInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task LogoutInternal() + { + //TODO: An exception here will lock the client into the unusable LoggingOut state. How should we handle? (Add same solution to both DiscordClients too) + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; + + try { _loginCancelToken?.Cancel(false); } + catch { } + + await DisconnectInternal().ConfigureAwait(false); + await _requestQueue.Clear().ConfigureAwait(false); + + await _requestQueue.SetCancelToken(CancellationToken.None).ConfigureAwait(false); + _restClient.SetCancelToken(CancellationToken.None); + + LoginState = LoginState.LoggedOut; + } + + public async Task Connect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task ConnectInternal() + { + if (LoginState != LoginState.LoggedIn) + throw new InvalidOperationException("You must log in before connecting."); + if (_gatewayClient == null) + throw new NotSupportedException("This client is not configured with websocket support."); + + ConnectionState = ConnectionState.Connecting; + try + { + _connectCancelToken = new CancellationTokenSource(); + if (_gatewayClient != null) + _gatewayClient.SetCancelToken(_connectCancelToken.Token); + + var gatewayResponse = await GetGateway().ConfigureAwait(false); + var url = $"{gatewayResponse.Url}?v={DiscordConfig.GatewayAPIVersion}&encoding={DiscordConfig.GatewayEncoding}"; + await _gatewayClient.Connect(url).ConfigureAwait(false); + + await SendIdentify().ConfigureAwait(false); + + ConnectionState = ConnectionState.Connected; + } + catch (Exception) + { + await DisconnectInternal().ConfigureAwait(false); + throw; } + } - _restClient.SetHeader("authorization", token); + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectInternal() + { + if (_gatewayClient == null) + throw new NotSupportedException("This client is not configured with websocket support."); + + if (ConnectionState == ConnectionState.Disconnected) return; + ConnectionState = ConnectionState.Disconnecting; + + try { _connectCancelToken?.Cancel(false); } + catch { } + + await _gatewayClient.Disconnect().ConfigureAwait(false); + + ConnectionState = ConnectionState.Disconnected; } //Core @@ -86,13 +257,13 @@ namespace Discord.API => SendInternal(method, endpoint, multipartArgs, true, bucket); public async Task Send(string method, string endpoint, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); + => DeserializeJson(await SendInternal(method, endpoint, null, false, bucket).ConfigureAwait(false)); public async Task Send(string method, string endpoint, object payload, GlobalBucket bucket = GlobalBucket.General) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, false, bucket).ConfigureAwait(false)); + => 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) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket).ConfigureAwait(false)); + => 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); @@ -102,14 +273,14 @@ namespace Discord.API => SendInternal(method, endpoint, multipartArgs, true, bucket, guildId); public async Task Send(string method, string endpoint, GuildBucket bucket, ulong guildId) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, null, false, bucket, guildId).ConfigureAwait(false)); + => 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) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, payload, false, bucket, guildId).ConfigureAwait(false)); + => 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) where TResponse : class - => Deserialize(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).ConfigureAwait(false)); - + => DeserializeJson(await SendInternal(method, endpoint, multipartArgs, false, bucket, guildId).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) @@ -124,36 +295,47 @@ namespace Discord.API var stopwatch = Stopwatch.StartNew(); string json = null; if (payload != null) - json = Serialize(payload); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); - int bytes = headerOnly ? 0 : (int)responseStream.Length; + json = SerializeJson(payload); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, json, headerOnly), group, bucketId, guildId).ConfigureAwait(false); stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } private async Task SendInternal(string method, string endpoint, IReadOnlyDictionary multipartArgs, bool headerOnly, BucketGroup group, int bucketId, ulong guildId) { var stopwatch = Stopwatch.StartNew(); - var responseStream = await _requestQueue.Send(new RestRequest(method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); + var responseStream = await _requestQueue.Send(new RestRequest(_restClient, method, endpoint, multipartArgs, headerOnly), group, bucketId, guildId).ConfigureAwait(false); int bytes = headerOnly ? 0 : (int)responseStream.Length; stopwatch.Stop(); double milliseconds = ToMilliseconds(stopwatch); - SentRequest?.Invoke(this, new SentRequestEventArgs(method, endpoint, bytes, milliseconds)); + await SentRequest.Raise(method, endpoint, milliseconds).ConfigureAwait(false); return responseStream; } - - //Auth - public async Task Login(LoginParams args) + 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) { - var response = await Send("POST", "auth/login", args).ConfigureAwait(false); - SetToken(TokenType.User, response.Token); + //TODO: Add ETF + byte[] bytes = null; + payload = new WebSocketMessage { Operation = 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); } + + //Auth public async Task ValidateToken() { await Send("GET", "auth/login").ConfigureAwait(false); @@ -164,11 +346,26 @@ namespace Discord.API { return await Send("GET", "gateway").ConfigureAwait(false); } + public async Task SendIdentify(int largeThreshold = 100, bool useCompression = true) + { + var props = new Dictionary + { + ["$device"] = "Discord.Net" + }; + var msg = new IdentifyParams() + { + Token = _authToken, + Properties = props, + LargeThreshold = largeThreshold, + UseCompression = useCompression + }; + await SendGateway(GatewayOpCodes.Identify, msg).ConfigureAwait(false); + } //Channels public async Task GetChannel(ulong channelId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { @@ -178,7 +375,8 @@ namespace Discord.API } public async Task GetChannel(ulong guildId, ulong channelId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); try { @@ -191,57 +389,58 @@ namespace Discord.API } public async Task> GetGuildChannels(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - - return await Send>("GET", $"guild/{guildId}/channels").ConfigureAwait(false); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return await Send>("GET", $"guilds/{guildId}/channels").ConfigureAwait(false); } public async Task CreateGuildChannel(ulong guildId, CreateGuildChannelParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.Bitrate <= 0) throw new ArgumentOutOfRangeException(nameof(args.Bitrate)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); + 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); } public async Task DeleteChannel(ulong channelId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); return await Send("DELETE", $"channels/{channelId}").ConfigureAwait(false); } public async Task ModifyGuildChannel(ulong channelId, ModifyGuildChannelParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); - if (args.Position < 0) throw new ArgumentOutOfRangeException(nameof(args.Position)); + 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); } public async Task ModifyGuildChannel(ulong channelId, ModifyTextChannelParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); - if (args.Position < 0) throw new ArgumentOutOfRangeException(nameof(args.Position)); + 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); } public async Task ModifyGuildChannel(ulong channelId, ModifyVoiceChannelParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args.Bitrate <= 0) throw new ArgumentOutOfRangeException(nameof(args.Bitrate)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); - if (args.Position < 0) throw new ArgumentOutOfRangeException(nameof(args.Position)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate)); + Preconditions.AtLeast(args.UserLimit, 0, nameof(args.Bitrate)); + Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); return await Send("PATCH", $"channels/{channelId}", args).ConfigureAwait(false); } public async Task ModifyGuildChannels(ulong guildId, IEnumerable args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); var channels = args.ToArray(); switch (channels.Length) @@ -260,7 +459,8 @@ namespace Discord.API //Channel Permissions public async Task ModifyChannelPermissions(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); + Preconditions.NotNull(args, nameof(args)); + await Send("PUT", $"channels/{channelId}/permissions/{targetId}", args).ConfigureAwait(false); } public async Task DeleteChannelPermission(ulong channelId, ulong targetId) @@ -271,7 +471,7 @@ namespace Discord.API //Guilds public async Task GetGuild(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { @@ -281,50 +481,50 @@ namespace Discord.API } public async Task CreateGuild(CreateGuildParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (string.IsNullOrEmpty(args.Name)) throw new ArgumentNullException(nameof(args.Name)); - if (string.IsNullOrEmpty(args.Region)) throw new ArgumentNullException(nameof(args.Region)); + 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); } public async Task DeleteGuild(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send("DELETE", $"guilds/{guildId}").ConfigureAwait(false); } public async Task LeaveGuild(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send("DELETE", $"users/@me/guilds/{guildId}").ConfigureAwait(false); } public async Task ModifyGuild(ulong guildId, ModifyGuildParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.AFKChannelId == 0) throw new ArgumentOutOfRangeException(nameof(args.AFKChannelId)); - if (args.AFKTimeout < 0) throw new ArgumentOutOfRangeException(nameof(args.AFKTimeout)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); - //if (args.OwnerId == 0) throw new ArgumentOutOfRangeException(nameof(args.OwnerId)); - //if (args.Region == "") throw new ArgumentOutOfRangeException(nameof(args.Region)); - if (args.VerificationLevel < 0) throw new ArgumentOutOfRangeException(nameof(args.VerificationLevel)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.AFKChannelId, 0, nameof(args.AFKChannelId)); + Preconditions.AtLeast(args.AFKTimeout, 0, nameof(args.AFKTimeout)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + Preconditions.NotNull(args.Owner, nameof(args.Owner)); + Preconditions.NotNull(args.Region, nameof(args.Region)); + Preconditions.AtLeast(args.VerificationLevel, 0, nameof(args.VerificationLevel)); return await Send("PATCH", $"guilds/{guildId}", args).ConfigureAwait(false); } public async Task BeginGuildPrune(ulong guildId, GuildPruneParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.Days < 0) throw new ArgumentOutOfRangeException(nameof(args.Days)); + 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); } public async Task GetGuildPruneCount(ulong guildId, GuildPruneParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.Days < 0) throw new ArgumentOutOfRangeException(nameof(args.Days)); + 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); } @@ -332,23 +532,23 @@ namespace Discord.API //Guild Bans public async Task> GetGuildBans(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + return await Send>("GET", $"guilds/{guildId}/bans").ConfigureAwait(false); } public async Task CreateGuildBan(ulong guildId, ulong userId, CreateGuildBanParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); - if (args.PruneDays < 0) throw new ArgumentOutOfRangeException(nameof(args.PruneDays)); + 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); } public async Task RemoveGuildBan(ulong guildId, ulong userId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); await Send("DELETE", $"guilds/{guildId}/bans/{userId}").ConfigureAwait(false); } @@ -356,7 +556,7 @@ namespace Discord.API //Guild Embeds public async Task GetGuildEmbed(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); try { @@ -366,8 +566,8 @@ namespace Discord.API } public async Task ModifyGuildEmbed(ulong guildId, ModifyGuildEmbedParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send("PATCH", $"guilds/{guildId}/embed", args).ConfigureAwait(false); } @@ -375,39 +575,39 @@ namespace Discord.API //Guild Integrations public async Task> GetGuildIntegrations(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send>("GET", $"guilds/{guildId}/integrations").ConfigureAwait(false); } public async Task CreateGuildIntegration(ulong guildId, CreateGuildIntegrationParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.Id == 0) throw new ArgumentOutOfRangeException(nameof(args.Id)); + 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); } public async Task DeleteGuildIntegration(ulong guildId, ulong integrationId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (integrationId == 0) throw new ArgumentOutOfRangeException(nameof(integrationId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); return await Send("DELETE", $"guilds/{guildId}/integrations/{integrationId}").ConfigureAwait(false); } public async Task ModifyGuildIntegration(ulong guildId, ulong integrationId, ModifyGuildIntegrationParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (integrationId == 0) throw new ArgumentOutOfRangeException(nameof(integrationId)); - if (args.ExpireBehavior < 0) throw new ArgumentOutOfRangeException(nameof(args.ExpireBehavior)); - if (args.ExpireGracePeriod < 0) throw new ArgumentOutOfRangeException(nameof(args.ExpireGracePeriod)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); + Preconditions.NotNull(args, nameof(args)); + 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); } public async Task SyncGuildIntegration(ulong guildId, ulong integrationId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (integrationId == 0) throw new ArgumentOutOfRangeException(nameof(integrationId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); return await Send("POST", $"guilds/{guildId}/integrations/{integrationId}/sync").ConfigureAwait(false); } @@ -415,7 +615,7 @@ namespace Discord.API //Guild Invites public async Task GetInvite(string inviteIdOrXkcd) { - if (string.IsNullOrEmpty(inviteIdOrXkcd)) throw new ArgumentOutOfRangeException(nameof(inviteIdOrXkcd)); + Preconditions.NotNullOrEmpty(inviteIdOrXkcd, nameof(inviteIdOrXkcd)); try { @@ -425,34 +625,34 @@ namespace Discord.API } public async Task> GetGuildInvites(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send>("GET", $"guilds/{guildId}/invites").ConfigureAwait(false); } public async Task GetChannelInvites(ulong channelId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); return await Send("GET", $"channels/{channelId}/invites").ConfigureAwait(false); } public async Task CreateChannelInvite(ulong channelId, CreateChannelInviteParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args.MaxAge < 0) throw new ArgumentOutOfRangeException(nameof(args.MaxAge)); - if (args.MaxUses < 0) throw new ArgumentOutOfRangeException(nameof(args.MaxUses)); + 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); } public async Task DeleteInvite(string inviteCode) { - if (string.IsNullOrEmpty(inviteCode)) throw new ArgumentOutOfRangeException(nameof(inviteCode)); + Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); return await Send("DELETE", $"invites/{inviteCode}").ConfigureAwait(false); } public async Task AcceptInvite(string inviteCode) { - if (string.IsNullOrEmpty(inviteCode)) throw new ArgumentOutOfRangeException(nameof(inviteCode)); + Preconditions.NotNullOrEmpty(inviteCode, nameof(inviteCode)); await Send("POST", $"invites/{inviteCode}").ConfigureAwait(false); } @@ -460,8 +660,8 @@ namespace Discord.API //Guild Members public async Task GetGuildMember(ulong guildId, ulong userId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); try { @@ -471,13 +671,13 @@ namespace Discord.API } public async Task> GetGuildMembers(ulong guildId, GetGuildMembersParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); - if (args.Offset < 0) throw new ArgumentOutOfRangeException(nameof(args.Offset)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtLeast(args.Offset, 0, nameof(args.Offset)); int limit = args.Limit.GetValueOrDefault(int.MaxValue); - int offset = args.Offset; + int offset = args.Offset.GetValueOrDefault(0); List result; if (args.Limit.IsSpecified) @@ -497,6 +697,7 @@ namespace Discord.API result.Add(models); limit -= DiscordConfig.MaxUsersPerBatch; + offset += models.Length; //Was this an incomplete (the last) batch? if (models.Length != DiscordConfig.MaxUsersPerBatch) break; @@ -511,55 +712,55 @@ namespace Discord.API } public async Task RemoveGuildMember(ulong guildId, ulong userId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); await Send("DELETE", $"guilds/{guildId}/members/{userId}").ConfigureAwait(false); } public async Task ModifyGuildMember(ulong guildId, ulong userId, ModifyGuildMemberParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); - - await Send("PATCH", $"guilds/{guildId}/members/{userId}", args).ConfigureAwait(false); + 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); } //Guild Roles public async Task> GetGuildRoles(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); - return await Send>("GET", $"guild/{guildId}/roles").ConfigureAwait(false); + return await Send>("GET", $"guilds/{guildId}/roles").ConfigureAwait(false); } public async Task CreateGuildRole(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send("POST", $"guilds/{guildId}/roles").ConfigureAwait(false); } public async Task DeleteGuildRole(ulong guildId, ulong roleId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (roleId == 0) throw new ArgumentOutOfRangeException(nameof(roleId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); await Send("DELETE", $"guilds/{guildId}/roles/{roleId}").ConfigureAwait(false); } public async Task ModifyGuildRole(ulong guildId, ulong roleId, ModifyGuildRoleParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (roleId == 0) throw new ArgumentOutOfRangeException(nameof(roleId)); - if (args.Color < 0) throw new ArgumentOutOfRangeException(nameof(args.Color)); - if (args.Name == "") throw new ArgumentOutOfRangeException(nameof(args.Name)); - if (args.Position < 0) throw new ArgumentOutOfRangeException(nameof(args.Position)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Color, 0, nameof(args.Color)); + 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); } public async Task> ModifyGuildRoles(ulong guildId, IEnumerable args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); var roles = args.ToArray(); switch (roles.Length) @@ -576,34 +777,33 @@ namespace Discord.API //Messages public async Task> GetChannelMessages(ulong channelId, GetChannelMessagesParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args.Limit <= 0) throw new ArgumentOutOfRangeException(nameof(args.Limit)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); int limit = args.Limit; - ulong? relativeId = args.RelativeMessageId; + ulong? relativeId = args.RelativeMessageId.IsSpecified ? args.RelativeMessageId.Value : (ulong?)null; string relativeDir = args.RelativeDirection == Direction.After ? "after" : "before"; - int runs = limit / DiscordConfig.MaxMessagesPerBatch; - int lastRunCount = limit - runs * DiscordConfig.MaxMessagesPerBatch; + int runs = (limit + DiscordConfig.MaxMessagesPerBatch - 1) / DiscordConfig.MaxMessagesPerBatch; + int lastRunCount = limit - (runs - 1) * DiscordConfig.MaxMessagesPerBatch; var result = new API.Message[runs][]; int i = 0; for (; i < runs; i++) { + int runCount = i == (runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; string endpoint; if (relativeId != null) - endpoint = $"channels/{channelId}/messages?limit={limit}&{relativeDir}={relativeId}"; + endpoint = $"channels/{channelId}/messages?limit={runCount}&{relativeDir}={relativeId}"; else - endpoint = $"channels/{channelId}/messages?limit={limit}"; + endpoint = $"channels/{channelId}/messages?limit={runCount}"; var models = await Send("GET", endpoint).ConfigureAwait(false); //Was this an empty batch? if (models.Length == 0) break; - result[i] = models; - - limit = (i == runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; + result[i] = models; relativeId = args.RelativeDirection == Direction.Before ? models[0].Id : models[models.Length - 1].Id; //Was this an incomplete (the last) batch? @@ -611,58 +811,101 @@ namespace Discord.API } if (i > 1) - return result.Take(i).SelectMany(x => x); + { + if (args.RelativeDirection == Direction.Before) + return result.Take(i).SelectMany(x => x); + else + return result.Take(i).Reverse().SelectMany(x => x); + } else if (i == 1) return result[0]; else return Array.Empty(); } - public Task CreateMessage(ulong channelId, CreateMessageParams args) - => CreateMessage(0, channelId, args); - public async Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args) + public Task CreateMessage(ulong guildId, ulong channelId, CreateMessageParams args) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return CreateMessageInternal(guildId, channelId, args); + } + public Task CreateDMMessage(ulong channelId, CreateMessageParams args) + { + return CreateMessageInternal(0, channelId, args); + } + public async Task CreateMessageInternal(ulong guildId, ulong channelId, CreateMessageParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); if (guildId != 0) return await Send("POST", $"channels/{channelId}/messages", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); else return await Send("POST", $"channels/{channelId}/messages", args, GlobalBucket.DirectMessage).ConfigureAwait(false); } - public Task UploadFile(ulong channelId, Stream file, UploadFileParams args) - => UploadFile(0, channelId, file, args); - public async Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args) + public Task UploadFile(ulong guildId, ulong channelId, Stream file, UploadFileParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return UploadFileInternal(guildId, channelId, file, args); + } + public Task UploadDMFile(ulong channelId, Stream file, UploadFileParams args) + { + return UploadFileInternal(0, channelId, file, args); + } + private async Task UploadFileInternal(ulong guildId, ulong channelId, Stream file, UploadFileParams args) + { + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + if (args.Content.IsSpecified) + { + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + } if (guildId != 0) return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); else - return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary()).ConfigureAwait(false); + return await Send("POST", $"channels/{channelId}/messages", file, args.ToDictionary(), GlobalBucket.DirectMessage).ConfigureAwait(false); + } + public Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return DeleteMessageInternal(guildId, channelId, messageId); + } + public Task DeleteDMMessage(ulong channelId, ulong messageId) + { + return DeleteMessageInternal(0, channelId, messageId); } - public Task DeleteMessage(ulong channelId, ulong messageId) - => DeleteMessage(0, channelId, messageId); - public async Task DeleteMessage(ulong guildId, ulong channelId, ulong messageId) + private async Task DeleteMessageInternal(ulong guildId, ulong channelId, ulong messageId) { - //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); + 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); else await Send("DELETE", $"channels/{channelId}/messages/{messageId}").ConfigureAwait(false); } - public Task DeleteMessages(ulong channelId, DeleteMessagesParam args) - => DeleteMessages(0, channelId, args); - public async Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParam args) + public Task DeleteMessages(ulong guildId, ulong channelId, DeleteMessagesParam args) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return DeleteMessagesInternal(guildId, channelId, args); + } + public Task DeleteDMMessages(ulong channelId, DeleteMessagesParam args) + { + return DeleteMessagesInternal(0, channelId, args); + } + private async Task DeleteMessagesInternal(ulong guildId, ulong channelId, DeleteMessagesParam args) { - //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (args == null) throw new ArgumentNullException(nameof(args)); - if (args.MessageIds == null) throw new ArgumentNullException(nameof(args.MessageIds)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.MessageIds, nameof(args.MessageIds)); var messageIds = args.MessageIds.ToArray(); switch (messageIds.Length) @@ -670,7 +913,7 @@ namespace Discord.API case 0: return; case 1: - await DeleteMessage(guildId, channelId, messageIds[0]).ConfigureAwait(false); + await DeleteMessageInternal(guildId, channelId, messageIds[0]).ConfigureAwait(false); break; default: if (guildId != 0) @@ -680,14 +923,27 @@ namespace Discord.API break; } } - public Task ModifyMessage(ulong channelId, ulong messageId, ModifyMessageParams args) - => ModifyMessage(0, channelId, messageId, args); - public async Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) + public Task ModifyMessage(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + return ModifyMessageInternal(guildId, channelId, messageId, args); + } + public Task ModifyDMMessage(ulong channelId, ulong messageId, ModifyMessageParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - //if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); + return ModifyMessageInternal(0, channelId, messageId, args); + } + private async Task ModifyMessageInternal(ulong guildId, ulong channelId, ulong messageId, ModifyMessageParams args) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNull(args, nameof(args)); + if (args.Content.IsSpecified) + { + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + } if (guildId != 0) return await Send("PATCH", $"channels/{channelId}/messages/{messageId}", args, GuildBucket.SendEditMessage, guildId).ConfigureAwait(false); @@ -696,14 +952,14 @@ namespace Discord.API } public async Task AckMessage(ulong channelId, ulong messageId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); - if (messageId == 0) throw new ArgumentOutOfRangeException(nameof(messageId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); await Send("POST", $"channels/{channelId}/messages/{messageId}/ack").ConfigureAwait(false); } public async Task TriggerTypingIndicator(ulong channelId) { - if (channelId == 0) throw new ArgumentOutOfRangeException(nameof(channelId)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); await Send("POST", $"channels/{channelId}/typing").ConfigureAwait(false); } @@ -711,7 +967,7 @@ namespace Discord.API //Users public async Task GetUser(ulong userId) { - if (userId == 0) throw new ArgumentOutOfRangeException(nameof(userId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); try { @@ -721,7 +977,7 @@ namespace Discord.API } public async Task GetUser(string username, ushort discriminator) { - if (string.IsNullOrEmpty(username)) throw new ArgumentOutOfRangeException(nameof(username)); + Preconditions.NotNullOrEmpty(username, nameof(username)); try { @@ -732,8 +988,8 @@ namespace Discord.API } public async Task> QueryUsers(string query, int limit) { - if (string.IsNullOrEmpty(query)) throw new ArgumentOutOfRangeException(nameof(query)); - if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit)); + Preconditions.NotNullOrEmpty(query, nameof(query)); + Preconditions.AtLeast(limit, 0, nameof(limit)); return await Send>("GET", $"users?q={Uri.EscapeDataString(query)}&limit={limit}").ConfigureAwait(false); } @@ -757,23 +1013,23 @@ namespace Discord.API } public async Task ModifyCurrentUser(ModifyCurrentUserParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (args.Email == "") throw new ArgumentOutOfRangeException(nameof(args.Email)); - if (args.Username == "") throw new ArgumentOutOfRangeException(nameof(args.Username)); + 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); } public async Task ModifyCurrentUserNick(ulong guildId, ModifyCurrentUserNickParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (args.Nickname == "") throw new ArgumentOutOfRangeException(nameof(args.Nickname)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEmpty(args.Nickname, nameof(args.Nickname)); await Send("PATCH", $"guilds/{guildId}/members/@me/nick", args).ConfigureAwait(false); } public async Task CreateDMChannel(CreateDMChannelParams args) { - if (args == null) throw new ArgumentNullException(nameof(args)); - if (args.RecipientId == 0) throw new ArgumentOutOfRangeException(nameof(args.RecipientId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(args.RecipientId, 0, nameof(args.RecipientId)); return await Send("POST", $"users/@me/channels", args).ConfigureAwait(false); } @@ -785,14 +1041,14 @@ namespace Discord.API } public async Task> GetGuildVoiceRegions(ulong guildId) { - if (guildId == 0) throw new ArgumentOutOfRangeException(nameof(guildId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); return await Send>("GET", $"guilds/{guildId}/regions").ConfigureAwait(false); } //Helpers private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - private string Serialize(object value) + private string SerializeJson(object value) { var sb = new StringBuilder(256); using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) @@ -800,7 +1056,7 @@ namespace Discord.API _serializer.Serialize(writer, value); return sb.ToString(); } - private T Deserialize(Stream jsonStream) + private T DeserializeJson(Stream jsonStream) { using (TextReader text = new StreamReader(jsonStream)) using (JsonReader reader = new JsonTextReader(text)) diff --git a/src/Discord.Net/API/Gateway/GatewayOpCodes.cs b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs new file mode 100644 index 000000000..82fbf51f3 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GatewayOpCodes.cs @@ -0,0 +1,24 @@ +namespace Discord.API.Gateway +{ + public enum GatewayOpCodes : byte + { + /// C←S - Used to send most events. + Dispatch = 0, + /// C↔S - Used to keep the connection alive and measure latency. + Heartbeat = 1, + /// C→S - Used to associate a connection with a token and specify configuration. + Identify = 2, + /// C→S - Used to update client's status and current game id. + StatusUpdate = 3, + /// C→S - Used to join a particular voice channel. + VoiceStateUpdate = 4, + /// C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. + VoiceServerPing = 5, + /// C→S - Used to resume a connection after a redirect occurs. + Resume = 6, + /// C←S - Used to notify a client that they must reconnect to another gateway. + Reconnect = 7, + /// C→S - Used to request all members that were withheld by large_threshold + RequestGuildMembers = 8 + } +} diff --git a/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs b/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs new file mode 100644 index 000000000..d6402731a --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildMembersChunkEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildMembersChunkEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("members")] + public GuildMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs new file mode 100644 index 000000000..f05543bf6 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildRoleCreateEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildRoleCreateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs new file mode 100644 index 000000000..345154432 --- /dev/null +++ b/src/Discord.Net/API/Gateway/GuildRoleUpdateEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class GuildRoleUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/IdentifyParams.cs b/src/Discord.Net/API/Gateway/IdentifyParams.cs new file mode 100644 index 000000000..8338e6e14 --- /dev/null +++ b/src/Discord.Net/API/Gateway/IdentifyParams.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API.Gateway +{ + public class IdentifyParams + { + [JsonProperty("token")] + public string Token { get; set; } + [JsonProperty("properties")] + public IDictionary Properties { get; set; } + [JsonProperty("large_threshold")] + public int LargeThreshold { get; set; } + [JsonProperty("compress")] + public bool UseCompression { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ReadyEvent.cs b/src/Discord.Net/API/Gateway/ReadyEvent.cs new file mode 100644 index 000000000..c0384c100 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ReadyEvent.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ReadyEvent + { + public class ReadState + { + [JsonProperty("id")] + public string ChannelId { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public string LastMessageId { get; set; } + } + + [JsonProperty("v")] + public int Version { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("read_state")] + public ReadState[] ReadStates { get; set; } + [JsonProperty("guilds")] + public Guild[] Guilds { get; set; } + [JsonProperty("private_channels")] + public Channel[] PrivateChannels { get; set; } + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + + //Ignored + [JsonProperty("user_settings")] + public object UserSettings { get; set; } + [JsonProperty("user_guild_settings")] + public object UserGuildSettings { get; set; } + [JsonProperty("tutorial")] + public object Tutorial { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/RequestMembersParams.cs b/src/Discord.Net/API/Gateway/RequestMembersParams.cs new file mode 100644 index 000000000..ed6edc6ef --- /dev/null +++ b/src/Discord.Net/API/Gateway/RequestMembersParams.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class RequestMembersParams + { + [JsonProperty("guild_id")] + public ulong[] GuildId { get; set; } + [JsonProperty("query")] + public string Query { get; set; } + [JsonProperty("limit")] + public int Limit { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ResumeParams.cs b/src/Discord.Net/API/Gateway/ResumeParams.cs new file mode 100644 index 000000000..ba4489336 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ResumeParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ResumeParams + { + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("seq")] + public uint Sequence { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/ResumedEvent.cs b/src/Discord.Net/API/Gateway/ResumedEvent.cs new file mode 100644 index 000000000..6087a0c38 --- /dev/null +++ b/src/Discord.Net/API/Gateway/ResumedEvent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class ResumedEvent + { + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/TypingStartEvent.cs b/src/Discord.Net/API/Gateway/TypingStartEvent.cs new file mode 100644 index 000000000..2e3829bc7 --- /dev/null +++ b/src/Discord.Net/API/Gateway/TypingStartEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class TypingStartEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("timestamp")] + public int Timestamp { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/UpdateStatusParams.cs b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs new file mode 100644 index 000000000..99e5ed7b8 --- /dev/null +++ b/src/Discord.Net/API/Gateway/UpdateStatusParams.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class UpdateStatusParams + { + [JsonProperty("idle_since")] + public long? IdleSince { get; set; } + [JsonProperty("game")] + public Game Game { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs new file mode 100644 index 000000000..d72d63548 --- /dev/null +++ b/src/Discord.Net/API/Gateway/UpdateVoiceParams.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class UpdateVoiceParams + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + [JsonProperty("self_mute")] + public bool IsSelfMuted { get; set; } + [JsonProperty("self_deaf")] + public bool IsSelfDeafened { get; set; } + } +} diff --git a/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs b/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs new file mode 100644 index 000000000..036d535c2 --- /dev/null +++ b/src/Discord.Net/API/Gateway/VoiceServerUpdateEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + public class VoiceServerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("endpoint")] + public string Endpoint { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/ImageAttribute.cs b/src/Discord.Net/API/ImageAttribute.cs new file mode 100644 index 000000000..c31c6d982 --- /dev/null +++ b/src/Discord.Net/API/ImageAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + public class ImageAttribute : Attribute { } +} diff --git a/src/Discord.Net/API/Int53Attribute.cs b/src/Discord.Net/API/Int53Attribute.cs new file mode 100644 index 000000000..7ac6f5467 --- /dev/null +++ b/src/Discord.Net/API/Int53Attribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + public class Int53Attribute : Attribute { } +} diff --git a/src/Discord.Net/API/Optional.cs b/src/Discord.Net/API/Optional.cs index e76d170e5..b828608b2 100644 --- a/src/Discord.Net/API/Optional.cs +++ b/src/Discord.Net/API/Optional.cs @@ -1,13 +1,15 @@ using System; +using System.Diagnostics; namespace Discord.API { //Based on https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Nullable.cs + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Optional : IOptional { private readonly T _value; - /// Gets the value for this paramter, or default(T) if unspecified. + /// Gets the value for this paramter. public T Value { get @@ -20,8 +22,6 @@ namespace Discord.API /// Returns true if this value has been specified. public bool IsSpecified { get; } - object IOptional.Value => _value; - /// Creates a new Parameter with the provided value. public Optional(T value) { @@ -30,7 +30,7 @@ namespace Discord.API } public T GetValueOrDefault() => _value; - public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : default(T); + public T GetValueOrDefault(T defaultValue) => IsSpecified ? _value : defaultValue; public override bool Equals(object other) { @@ -38,11 +38,14 @@ namespace Discord.API if (other == null) return false; return _value.Equals(other); } - public override int GetHashCode() => IsSpecified ? _value.GetHashCode() : 0; - public override string ToString() => IsSpecified ? _value.ToString() : ""; + + public override string ToString() => IsSpecified ? _value?.ToString() : null; + private string DebuggerDisplay => IsSpecified ? _value.ToString() : ""; public static implicit operator Optional(T value) => new Optional(value); - public static implicit operator T(Optional value) => value.Value; + public static explicit operator T(Optional value) => value.Value; + + object IOptional.Value => Value; } } diff --git a/src/Discord.Net/API/Rest/CreateGuildParams.cs b/src/Discord.Net/API/Rest/CreateGuildParams.cs index dd6e5b8fd..09c294b5e 100644 --- a/src/Discord.Net/API/Rest/CreateGuildParams.cs +++ b/src/Discord.Net/API/Rest/CreateGuildParams.cs @@ -1,5 +1,4 @@ -using Discord.Net.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; using System.IO; namespace Discord.API.Rest @@ -11,7 +10,7 @@ namespace Discord.API.Rest [JsonProperty("region")] public string Region { get; set; } - [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + [JsonProperty("icon"), Image] public Optional Icon { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs index f102cccca..31a41086a 100644 --- a/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net/API/Rest/ModifyChannelPermissionsParams.cs @@ -5,8 +5,8 @@ namespace Discord.API.Rest public class ModifyChannelPermissionsParams { [JsonProperty("allow")] - public Optional Allow { get; set; } + public Optional Allow { get; set; } [JsonProperty("deny")] - public Optional Deny { get; set; } + public Optional Deny { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs index 64aab3181..a29a9c8b4 100644 --- a/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs +++ b/src/Discord.Net/API/Rest/ModifyCurrentUserParams.cs @@ -14,7 +14,7 @@ namespace Discord.API.Rest public Optional Password { get; set; } [JsonProperty("new_password")] public Optional NewPassword { get; set; } - [JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] + [JsonProperty("avatar"), Image] public Optional Avatar { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs index 8444bb598..23d498f25 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildChannelsParams.cs @@ -5,7 +5,7 @@ namespace Discord.API.Rest public class ModifyGuildChannelsParams { [JsonProperty("id")] - public Optional Id { get; set; } + public ulong Id { get; set; } [JsonProperty("position")] public Optional Position { get; set; } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildParams.cs b/src/Discord.Net/API/Rest/ModifyGuildParams.cs index e92b1f63c..6e7ff2e34 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildParams.cs @@ -16,11 +16,11 @@ namespace Discord.API.Rest public Optional AFKChannelId { get; set; } [JsonProperty("afk_timeout")] public Optional AFKTimeout { get; set; } - [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + [JsonProperty("icon"), Image] public Optional Icon { get; set; } [JsonProperty("owner_id")] public Optional Owner { get; set; } - [JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] + [JsonProperty("splash"), Image] public Optional Splash { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs index 58a715ae9..d3b6979ec 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildRoleParams.cs @@ -7,7 +7,7 @@ namespace Discord.API.Rest [JsonProperty("name")] public Optional Name { get; set; } [JsonProperty("permissions")] - public Optional Permissions { get; set; } + public Optional Permissions { get; set; } [JsonProperty("position")] public Optional Position { get; set; } [JsonProperty("color")] diff --git a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs index 286c2463d..7002079d5 100644 --- a/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildRolesParams.cs @@ -5,6 +5,6 @@ namespace Discord.API.Rest public class ModifyGuildRolesParams : ModifyGuildRoleParams { [JsonProperty("id")] - public Optional Id { get; set; } + public ulong Id { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs b/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs index b268783c8..8d449c607 100644 --- a/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs +++ b/src/Discord.Net/API/Rest/ModifyVoiceChannelParams.cs @@ -6,5 +6,7 @@ namespace Discord.API.Rest { [JsonProperty("bitrate")] public Optional Bitrate { get; set; } + [JsonProperty("user_limit")] + public Optional UserLimit { get; set; } } } diff --git a/src/Discord.Net/API/Voice/VoiceOpCodes.cs b/src/Discord.Net/API/Voice/VoiceOpCodes.cs new file mode 100644 index 000000000..73c7eda0c --- /dev/null +++ b/src/Discord.Net/API/Voice/VoiceOpCodes.cs @@ -0,0 +1,18 @@ +namespace Discord.API.Gateway +{ + public enum VoiceOpCodes : byte + { + /// C→S - Used to associate a connection with a token. + Identify = 0, + /// C→S - Used to specify configuration. + SelectProtocol = 1, + /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. + Ready = 2, + /// C↔S - Used to keep the connection alive and measure latency. + Heartbeat = 3, + /// C←S - Used to provide an encryption key to the client. + SessionDescription = 4, + /// C↔S - Used to inform that a certain user is speaking. + Speaking = 5 + } +} diff --git a/src/Discord.Net/API/WebSocketMessage.cs b/src/Discord.Net/API/WebSocketMessage.cs new file mode 100644 index 000000000..19ec2ac41 --- /dev/null +++ b/src/Discord.Net/API/WebSocketMessage.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class WebSocketMessage + { + [JsonProperty("op")] + public int Operation { get; set; } + [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] + public uint? Sequence { get; set; } + [JsonProperty("d")] + public object Payload { get; set; } + } +} diff --git a/src/Discord.Net/Common/Entities/Guilds/IIntegrationAccount.cs b/src/Discord.Net/Common/Entities/Guilds/IIntegrationAccount.cs deleted file mode 100644 index 7e5052c1b..000000000 --- a/src/Discord.Net/Common/Entities/Guilds/IIntegrationAccount.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IIntegrationAccount : IEntity - { - string Name { get; } - } -} diff --git a/src/Discord.Net/Common/Entities/Invites/IPublicInvite.cs b/src/Discord.Net/Common/Entities/Invites/IPublicInvite.cs deleted file mode 100644 index 1d518bd0a..000000000 --- a/src/Discord.Net/Common/Entities/Invites/IPublicInvite.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Discord -{ - public interface IPublicInvite : IInvite - { - /// Gets the name of the the channel this invite is linked to. - string ChannelName { get; } - /// Gets the name of the guild this invite is linked to. - string GuildName { get; } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs deleted file mode 100644 index ffcc403cf..000000000 --- a/src/Discord.Net/Common/Entities/Permissions/ChannelPermissions.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Discord -{ - public struct ChannelPermissions - { - private static ChannelPermissions _allDM { get; } = new ChannelPermissions(0b000100_000000_0011111111_0000011001); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(0b000000_000000_0001110011_0000000000); - private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(0b000100_111111_0000000000_0000011001); - - /// Gets a blank ChannelPermissions that grants no permissions. - public static ChannelPermissions None { get; } = new ChannelPermissions(); - /// Gets a ChannelPermissions that grants all permissions for a given channelType. - public static ChannelPermissions All(IChannel channel) - { - switch (channel) - { - case ITextChannel _: return _allText; - case IVoiceChannel _: return _allVoice; - case IDMChannel _: return _allDM; - default: - throw new ArgumentException("Unknown channel type", nameof(channel)); - } - } - - /// Gets a packed value representing all the permissions in this ChannelPermissions. - public uint RawValue { get; } - - /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionUtilities.GetValue(RawValue, ChannelPermission.CreateInstantInvite); - /// If True, a user may create, delete and modify this channel. - public bool ManageChannel => PermissionUtilities.GetValue(RawValue, ChannelPermission.ManageChannel); - - /// If True, a user may join channels. - public bool ReadMessages => PermissionUtilities.GetValue(RawValue, ChannelPermission.ReadMessages); - /// If True, a user may send messages. - public bool SendMessages => PermissionUtilities.GetValue(RawValue, ChannelPermission.SendMessages); - /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionUtilities.GetValue(RawValue, ChannelPermission.SendTTSMessages); - /// If True, a user may delete messages. - public bool ManageMessages => PermissionUtilities.GetValue(RawValue, ChannelPermission.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionUtilities.GetValue(RawValue, ChannelPermission.EmbedLinks); - /// If True, a user may send files. - public bool AttachFiles => PermissionUtilities.GetValue(RawValue, ChannelPermission.AttachFiles); - /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionUtilities.GetValue(RawValue, ChannelPermission.ReadMessageHistory); - /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionUtilities.GetValue(RawValue, ChannelPermission.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public bool Connect => PermissionUtilities.GetValue(RawValue, ChannelPermission.Connect); - /// If True, a user may speak in a voice channel. - public bool Speak => PermissionUtilities.GetValue(RawValue, ChannelPermission.Speak); - /// If True, a user may mute users. - public bool MuteMembers => PermissionUtilities.GetValue(RawValue, ChannelPermission.MuteMembers); - /// If True, a user may deafen users. - public bool DeafenMembers => PermissionUtilities.GetValue(RawValue, ChannelPermission.DeafenMembers); - /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionUtilities.GetValue(RawValue, ChannelPermission.MoveMembers); - /// If True, a user may use voice-activity-detection rather than push-to-talk. - public bool UseVAD => PermissionUtilities.GetValue(RawValue, ChannelPermission.UseVAD); - - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public bool ManagePermissions => PermissionUtilities.GetValue(RawValue, ChannelPermission.ManagePermissions); - - /// Creates a new ChannelPermissions with the provided packed value. - public ChannelPermissions(uint rawValue) { RawValue = rawValue; } - - private ChannelPermissions(uint initialValue, bool? createInstantInvite = null, bool? manageChannel = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) - { - uint value = initialValue; - - PermissionUtilities.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); - PermissionUtilities.SetValue(ref value, manageChannel, ChannelPermission.ManageChannel); - PermissionUtilities.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); - PermissionUtilities.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); - PermissionUtilities.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); - PermissionUtilities.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); - PermissionUtilities.SetValue(ref value, embedLinks, ChannelPermission.EmbedLinks); - PermissionUtilities.SetValue(ref value, attachFiles, ChannelPermission.AttachFiles); - PermissionUtilities.SetValue(ref value, readMessageHistory, ChannelPermission.ReadMessageHistory); - PermissionUtilities.SetValue(ref value, mentionEveryone, ChannelPermission.MentionEveryone); - PermissionUtilities.SetValue(ref value, connect, ChannelPermission.Connect); - PermissionUtilities.SetValue(ref value, speak, ChannelPermission.Speak); - PermissionUtilities.SetValue(ref value, muteMembers, ChannelPermission.MuteMembers); - PermissionUtilities.SetValue(ref value, deafenMembers, ChannelPermission.DeafenMembers); - PermissionUtilities.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); - PermissionUtilities.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); - PermissionUtilities.SetValue(ref value, managePermissions, ChannelPermission.ManagePermissions); - - RawValue = value; - } - - /// Creates a new ChannelPermissions with the provided permissions. - public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, - bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, - bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, - bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, - bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false) - : this(0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions) { } - - /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, managePermissions); - - /// - public override string ToString() - { - var perms = new List(); - int x = 1; - for (byte i = 0; i < 32; i++, x <<= 1) - { - if ((RawValue & x) != 0) - { - if (Enum.IsDefined(typeof(ChannelPermission), i)) - perms.Add($"{(ChannelPermission)i}"); - } - } - return string.Join(", ", perms); - } - } -} diff --git a/src/Discord.Net/Common/Entities/Permissions/PermissionUtilities.cs b/src/Discord.Net/Common/Entities/Permissions/PermissionUtilities.cs deleted file mode 100644 index 66ad57374..000000000 --- a/src/Discord.Net/Common/Entities/Permissions/PermissionUtilities.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Discord -{ - internal static class PermissionUtilities - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(uint allow, uint deny, ChannelPermission bit) - => GetValue(allow, deny, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(uint allow, uint deny, GuildPermission bit) - => GetValue(allow, deny, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(uint allow, uint deny, byte bit) - { - if (HasBit(allow, bit)) - return PermValue.Allow; - else if (HasBit(deny, bit)) - return PermValue.Deny; - else - return PermValue.Inherit; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(uint value, ChannelPermission bit) - => GetValue(value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(uint value, GuildPermission bit) - => GetValue(value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(uint value, byte bit) => HasBit(value, bit); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint rawValue, bool? value, ChannelPermission bit) - => SetValue(ref rawValue, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint rawValue, bool? value, GuildPermission bit) - => SetValue(ref rawValue, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint rawValue, bool? value, byte bit) - { - if (value.HasValue) - { - if (value == true) - SetBit(ref rawValue, bit); - else - UnsetBit(ref rawValue, bit); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint allow, ref uint deny, PermValue? value, ChannelPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint allow, ref uint deny, PermValue? value, GuildPermission bit) - => SetValue(ref allow, ref deny, value, (byte)bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint allow, ref uint deny, PermValue? value, byte bit) - { - if (value.HasValue) - { - switch (value) - { - case PermValue.Allow: - SetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - case PermValue.Deny: - UnsetBit(ref allow, bit); - SetBit(ref deny, bit); - break; - default: - UnsetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasBit(uint value, byte bit) => (value & (1U << bit)) != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetBit(ref uint value, byte bit) => value |= (1U << bit); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnsetBit(ref uint value, byte bit) => value &= ~(1U << bit); - } -} diff --git a/src/Discord.Net/Common/Entities/Users/IDMUser.cs b/src/Discord.Net/Common/Entities/Users/IDMUser.cs deleted file mode 100644 index e8fdd1f19..000000000 --- a/src/Discord.Net/Common/Entities/Users/IDMUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public interface IDMUser : IUser - { - /// Gets the private channel with this user. - IDMChannel Channel { get; } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Common/Events/SentRequestEventArgs.cs b/src/Discord.Net/Common/Events/SentRequestEventArgs.cs deleted file mode 100644 index c62c4d917..000000000 --- a/src/Discord.Net/Common/Events/SentRequestEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Discord.Net.Rest -{ - public class SentRequestEventArgs : EventArgs - { - public string Method { get; } - public string Endpoint { get; } - public int ResponseLength { get; } - public double Milliseconds { get; } - - public SentRequestEventArgs(string method, string endpoint, int responseLength, double milliseconds) - { - Method = method; - Endpoint = endpoint; - ResponseLength = responseLength; - Milliseconds = milliseconds; - } - } -} diff --git a/src/Discord.Net/Common/Helpers/EventExtensions.cs b/src/Discord.Net/Common/Helpers/EventExtensions.cs deleted file mode 100644 index a024822b7..000000000 --- a/src/Discord.Net/Common/Helpers/EventExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord -{ - internal static class EventExtensions - { - public static void Raise(this EventHandler eventHandler, object sender) - => eventHandler?.Invoke(sender, EventArgs.Empty); - public static void Raise(this EventHandler eventHandler, object sender, T eventArgs) - where T : EventArgs - => eventHandler?.Invoke(sender, eventArgs); - } -} diff --git a/src/Discord.Net/Common/Helpers/PermissionHelper.cs b/src/Discord.Net/Common/Helpers/PermissionHelper.cs deleted file mode 100644 index 780aedbf8..000000000 --- a/src/Discord.Net/Common/Helpers/PermissionHelper.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Discord -{ - public static class PermissionHelper - { - public static uint Resolve(IGuildUser user) - { - var roles = user.Roles; - uint newPermissions = 0; - for (int i = 0; i < roles.Count; i++) - newPermissions |= roles[i].Permissions.RawValue; - return newPermissions; - } - - public static uint Resolve(IGuildUser user, IGuildChannel channel) - { - uint resolvedPermissions = 0; - - uint mask = ChannelPermissions.All(channel).RawValue; - if (user.Id == user.Guild.OwnerId || PermissionUtilities.GetValue(resolvedPermissions, GuildPermission.Administrator)) - resolvedPermissions = mask; //Owners and administrators always have all permissions - else - { - //Start with this user's guild permissions - resolvedPermissions = Resolve(user); - var overwrites = channel.PermissionOverwrites; - - Overwrite entry; - var roles = user.Roles; - if (roles.Count > 0) - { - for (int i = 0; i < roles.Count; i++) - { - 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; - } - } - if (overwrites.TryGetValue(user.Id, out entry)) - resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; - - switch (channel) - { - case ITextChannel _: - if (!PermissionUtilities.GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - case IVoiceChannel _: - if (!PermissionUtilities.GetValue(resolvedPermissions, ChannelPermission.Connect)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - break; - default: - resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) - break; - } - } - - return resolvedPermissions; - } - } -} diff --git a/src/Discord.Net/ConcurrentHashSet.cs b/src/Discord.Net/ConcurrentHashSet.cs new file mode 100644 index 000000000..18572cfcf --- /dev/null +++ b/src/Discord.Net/ConcurrentHashSet.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Threading; + +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 + { + private sealed class Tables + { + internal readonly Node[] _buckets; + internal readonly object[] _locks; + internal volatile int[] _countPerLock; + + internal Tables(Node[] buckets, object[] locks, int[] countPerLock) + { + _buckets = buckets; + _locks = locks; + _countPerLock = countPerLock; + } + } + private sealed class Node + { + internal readonly T _value; + internal volatile Node _next; + internal readonly int _hashcode; + + internal Node(T key, int hashcode, Node next) + { + _value = key; + _next = next; + _hashcode = hashcode; + } + } + + private const int DefaultCapacity = 31; + private const int MaxLockNumber = 1024; + + private static int GetBucket(int hashcode, int bucketCount) + { + int bucketNo = (hashcode & 0x7fffffff) % bucketCount; + return bucketNo; + } + private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) + { + bucketNo = (hashcode & 0x7fffffff) % bucketCount; + lockNo = bucketNo % lockCount; + } + private static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount; + + private volatile Tables _tables; + private readonly IEqualityComparer _comparer; + private readonly bool _growLockArray; + private int _budget; + + public int Count + { + get + { + int count = 0; + + int acquiredLocks = 0; + try + { + AcquireAllLocks(ref acquiredLocks); + + for (int i = 0; i < _tables._countPerLock.Length; i++) + count += _tables._countPerLock[i]; + } + finally { ReleaseLocks(0, acquiredLocks); } + + return count; + } + } + public bool IsEmpty + { + get + { + int acquiredLocks = 0; + try + { + // Acquire all locks + AcquireAllLocks(ref acquiredLocks); + + for (int i = 0; i < _tables._countPerLock.Length; i++) + { + if (_tables._countPerLock[i] != 0) + return false; + } + } + finally { ReleaseLocks(0, acquiredLocks); } + + return true; + } + } + public ReadOnlyCollection Values + { + get + { + int locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + List values = new List(); + + for (int i = 0; i < _tables._buckets.Length; i++) + { + Node current = _tables._buckets[i]; + while (current != null) + { + values.Add(current._value); + current = current._next; + } + } + + return new ReadOnlyCollection(values); + } + finally { ReleaseLocks(0, locksAcquired); } + } + } + + public ConcurrentHashSet() + : this(DefaultConcurrencyLevel, DefaultCapacity, true, EqualityComparer.Default) { } + public ConcurrentHashSet(int concurrencyLevel, int capacity) + : this(concurrencyLevel, capacity, false, EqualityComparer.Default) { } + public ConcurrentHashSet(IEnumerable collection) + : this(collection, EqualityComparer.Default) { } + public ConcurrentHashSet(IEqualityComparer comparer) + : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { } + public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) + : this(comparer) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + InitializeFromCollection(collection); + } + public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) + : this(concurrencyLevel, DefaultCapacity, false, comparer) + { + if (collection == null) throw new ArgumentNullException(nameof(collection)); + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + InitializeFromCollection(collection); + } + public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer comparer) + : this(concurrencyLevel, capacity, false, comparer) { } + internal ConcurrentHashSet(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer comparer) + { + if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel)); + if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + + if (capacity < concurrencyLevel) + capacity = concurrencyLevel; + + object[] locks = new object[concurrencyLevel]; + for (int i = 0; i < locks.Length; i++) + locks[i] = new object(); + + int[] countPerLock = new int[locks.Length]; + Node[] buckets = new Node[capacity]; + _tables = new Tables(buckets, locks, countPerLock); + + _comparer = comparer; + _growLockArray = growLockArray; + _budget = buckets.Length / locks.Length; + } + private void InitializeFromCollection(IEnumerable collection) + { + foreach (var value in collection) + { + if (value == null) throw new ArgumentNullException("key"); + + if (!TryAddInternal(value, _comparer.GetHashCode(value), false)) + throw new ArgumentException(); + } + + if (_budget == 0) + _budget = _tables._buckets.Length / _tables._locks.Length; + } + + public bool ContainsKey(T value) + { + if (value == null) throw new ArgumentNullException("key"); + return ContainsKeyInternal(value, _comparer.GetHashCode(value)); + } + private bool ContainsKeyInternal(T value, int hashcode) + { + Tables tables = _tables; + + int bucketNo = GetBucket(hashcode, tables._buckets.Length); + + Node n = Volatile.Read(ref tables._buckets[bucketNo]); + + while (n != null) + { + if (hashcode == n._hashcode && _comparer.Equals(n._value, value)) + return true; + n = n._next; + } + + return false; + } + + public bool TryAdd(T value) + { + if (value == null) throw new ArgumentNullException("key"); + return TryAddInternal(value, _comparer.GetHashCode(value), true); + } + private bool TryAddInternal(T value, int hashcode, bool acquireLock) + { + while (true) + { + int bucketNo, lockNo; + + Tables tables = _tables; + GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + + bool resizeDesired = false; + bool lockTaken = false; + try + { + if (acquireLock) + Monitor.Enter(tables._locks[lockNo], ref lockTaken); + + if (tables != _tables) + continue; + + Node prev = null; + for (Node node = tables._buckets[bucketNo]; node != null; node = node._next) + { + if (hashcode == node._hashcode && _comparer.Equals(node._value, value)) + return false; + prev = node; + } + + Volatile.Write(ref tables._buckets[bucketNo], new Node(value, hashcode, tables._buckets[bucketNo])); + checked { tables._countPerLock[lockNo]++; } + + if (tables._countPerLock[lockNo] > _budget) + resizeDesired = true; + } + finally + { + if (lockTaken) + Monitor.Exit(tables._locks[lockNo]); + } + + if (resizeDesired) + GrowTable(tables); + + return true; + } + } + + public bool TryRemove(T value) + { + if (value == null) throw new ArgumentNullException("key"); + return TryRemoveInternal(value); + } + private bool TryRemoveInternal(T value) + { + int hashcode = _comparer.GetHashCode(value); + while (true) + { + Tables tables = _tables; + + int bucketNo, lockNo; + GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + + lock (tables._locks[lockNo]) + { + if (tables != _tables) + continue; + + Node prev = null; + for (Node curr = tables._buckets[bucketNo]; curr != null; curr = curr._next) + { + if (hashcode == curr._hashcode && _comparer.Equals(curr._value, value)) + { + if (prev == null) + Volatile.Write(ref tables._buckets[bucketNo], curr._next); + else + prev._next = curr._next; + + value = curr._value; + tables._countPerLock[lockNo]--; + return true; + } + prev = curr; + } + } + + value = default(T); + return false; + } + } + + public void Clear() + { + int locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + + Tables newTables = new Tables(new Node[DefaultCapacity], _tables._locks, new int[_tables._countPerLock.Length]); + _tables = newTables; + _budget = Math.Max(1, newTables._buckets.Length / newTables._locks.Length); + } + finally + { + ReleaseLocks(0, locksAcquired); + } + } + + public IEnumerator GetEnumerator() + { + Node[] buckets = _tables._buckets; + + for (int i = 0; i < buckets.Length; i++) + { + Node current = Volatile.Read(ref buckets[i]); + + while (current != null) + { + yield return current._value; + current = current._next; + } + } + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private void GrowTable(Tables tables) + { + const int MaxArrayLength = 0X7FEFFFFF; + int locksAcquired = 0; + try + { + AcquireLocks(0, 1, ref locksAcquired); + if (tables != _tables) + return; + + long approxCount = 0; + for (int i = 0; i < tables._countPerLock.Length; i++) + approxCount += tables._countPerLock[i]; + + if (approxCount < tables._buckets.Length / 4) + { + _budget = 2 * _budget; + if (_budget < 0) + _budget = int.MaxValue; + return; + } + + int newLength = 0; + bool maximizeTableSize = false; + try + { + checked + { + newLength = tables._buckets.Length * 2 + 1; + while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) + newLength += 2; + + if (newLength > MaxArrayLength) + maximizeTableSize = true; + } + } + catch (OverflowException) + { + maximizeTableSize = true; + } + + if (maximizeTableSize) + { + newLength = MaxArrayLength; + _budget = int.MaxValue; + } + + AcquireLocks(1, tables._locks.Length, ref locksAcquired); + + object[] newLocks = tables._locks; + + if (_growLockArray && tables._locks.Length < MaxLockNumber) + { + newLocks = new object[tables._locks.Length * 2]; + Array.Copy(tables._locks, 0, newLocks, 0, tables._locks.Length); + for (int i = tables._locks.Length; i < newLocks.Length; i++) + newLocks[i] = new object(); + } + + Node[] newBuckets = new Node[newLength]; + int[] newCountPerLock = new int[newLocks.Length]; + + for (int i = 0; i < tables._buckets.Length; i++) + { + Node current = tables._buckets[i]; + while (current != null) + { + Node next = current._next; + int newBucketNo, newLockNo; + GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length); + + newBuckets[newBucketNo] = new Node(current._value, current._hashcode, newBuckets[newBucketNo]); + + checked { newCountPerLock[newLockNo]++; } + + current = next; + } + } + + _budget = Math.Max(1, newBuckets.Length / newLocks.Length); + _tables = new Tables(newBuckets, newLocks, newCountPerLock); + } + finally { ReleaseLocks(0, locksAcquired); } + } + + private void AcquireAllLocks(ref int locksAcquired) + { + AcquireLocks(0, 1, ref locksAcquired); + AcquireLocks(1, _tables._locks.Length, ref locksAcquired); + } + private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) + { + object[] locks = _tables._locks; + + for (int i = fromInclusive; i < toExclusive; i++) + { + bool lockTaken = false; + try + { + Monitor.Enter(locks[i], ref lockTaken); + } + finally + { + if (lockTaken) + locksAcquired++; + } + } + } + private void ReleaseLocks(int fromInclusive, int toExclusive) + { + for (int i = fromInclusive; i < toExclusive; i++) + Monitor.Exit(_tables._locks[i]); + } + } + + //https://github.com/dotnet/corefx/blob/d0dc5fc099946adc1035b34a8b1f6042eddb0c75/src/System.Threading.Tasks.Parallel/src/System/Threading/PlatformHelper.cs + //Copyright (c) .NET Foundation and Contributors + internal static class PlatformHelper + { + private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000; + private static volatile int s_processorCount; + private static volatile int s_lastProcessorCountRefreshTicks; + + internal static int ProcessorCount + { + get + { + int now = Environment.TickCount; + if (s_processorCount == 0 || (now - s_lastProcessorCountRefreshTicks) >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS) + { + s_processorCount = Environment.ProcessorCount; + s_lastProcessorCountRefreshTicks = now; + } + + return s_processorCount; + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net/ConnectionState.cs b/src/Discord.Net/ConnectionState.cs new file mode 100644 index 000000000..42c505ccd --- /dev/null +++ b/src/Discord.Net/ConnectionState.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum ConnectionState : byte + { + Disconnected, + Connecting, + Connected, + Disconnecting + } +} diff --git a/src/Discord.Net/Common/Helpers/DateTimeHelper.cs b/src/Discord.Net/DateTimeUtils.cs similarity index 73% rename from src/Discord.Net/Common/Helpers/DateTimeHelper.cs rename to src/Discord.Net/DateTimeUtils.cs index a93efcb1d..92a42e74b 100644 --- a/src/Discord.Net/Common/Helpers/DateTimeHelper.cs +++ b/src/Discord.Net/DateTimeUtils.cs @@ -2,9 +2,10 @@ namespace Discord { - internal static class DateTimeHelper + internal static class DateTimeUtils { private const ulong EpochTicks = 621355968000000000UL; + private const ulong DiscordEpochMillis = 1420070400000UL; public static DateTime FromEpochMilliseconds(ulong value) => new DateTime((long)(value * TimeSpan.TicksPerMillisecond + EpochTicks), DateTimeKind.Utc); @@ -12,6 +13,6 @@ namespace Discord => new DateTime((long)(value * TimeSpan.TicksPerSecond + EpochTicks), DateTimeKind.Utc); public static DateTime FromSnowflake(ulong value) - => FromEpochMilliseconds((value >> 22) + 1420070400000UL); + => FromEpochMilliseconds((value >> 22) + DiscordEpochMillis); } } diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj deleted file mode 100644 index 48f2a4928..000000000 --- a/src/Discord.Net/Discord.Net.csproj +++ /dev/null @@ -1,223 +0,0 @@ - - - - - Debug - AnyCPU - {18F6FE23-73F6-4CA6-BBD9-F0139DC3EE90} - Library - Properties - Discord - Discord.Net - v4.6.1 - 512 - - - - true - full - false - bin\Debug\ - TRACE;DEBUG;__DEMO__,__DEMO_EXPERIMENTAL__ - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj new file mode 100644 index 000000000..6759e09b4 --- /dev/null +++ b/src/Discord.Net/Discord.Net.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 91e9e7bd-75c9-4e98-84aa-2c271922e5c2 + Discord + .\obj + .\bin\ + v4.5.2 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index eef0bc638..35e6f010b 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -3,12 +3,15 @@ using System.Reflection; namespace Discord { + //TODO: Add socket config items in their own class + public class DiscordConfig { public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - public const int GatewayAPIVersion = 3; + public const int GatewayAPIVersion = 3; //TODO: Upgrade to 4 + public const string GatewayEncoding = "json"; public const string ClientAPIUrl = "https://discordapp.com/api/"; public const string CDNUrl = "https://cdn.discordapp.com/"; @@ -26,6 +29,6 @@ namespace Discord public LogSeverity LogLevel { get; set; } = LogSeverity.Info; /// Gets or sets the provider used to generate new REST connections. - public RestClientProvider RestClientProvider { get; set; } = (url, ct) => new DefaultRestClient(url, ct); + public RestClientProvider RestClientProvider { get; set; } = url => new DefaultRestClient(url); } } diff --git a/src/Discord.Net/Common/Entities/Channels/ChannelType.cs b/src/Discord.Net/Entities/Channels/ChannelType.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/ChannelType.cs rename to src/Discord.Net/Entities/Channels/ChannelType.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs similarity index 71% rename from src/Discord.Net/Common/Entities/Channels/IChannel.cs rename to src/Discord.Net/Entities/Channels/IChannel.cs index 9540f4d26..74ab4a2f2 100644 --- a/src/Discord.Net/Common/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -7,6 +7,8 @@ namespace Discord { /// Gets a collection of all users in this channel. Task> GetUsers(); + /// Gets a paginated collection of all users in this channel. + 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/Common/Entities/Channels/IDMChannel.cs b/src/Discord.Net/Entities/Channels/IDMChannel.cs similarity index 90% rename from src/Discord.Net/Common/Entities/Channels/IDMChannel.cs rename to src/Discord.Net/Entities/Channels/IDMChannel.cs index f6b53a91f..5038bf36c 100644 --- a/src/Discord.Net/Common/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net/Entities/Channels/IDMChannel.cs @@ -5,7 +5,7 @@ namespace Discord public interface IDMChannel : IMessageChannel, IUpdateable { /// Gets the recipient of all messages in this channel. - IDMUser Recipient { get; } + IUser Recipient { get; } /// Closes this private channel, removing it from your channel list. Task Close(); diff --git a/src/Discord.Net/Common/Entities/Channels/IGuildChannel.cs b/src/Discord.Net/Entities/Channels/IGuildChannel.cs similarity index 93% rename from src/Discord.Net/Common/Entities/Channels/IGuildChannel.cs rename to src/Discord.Net/Entities/Channels/IGuildChannel.cs index 456f6931e..0a6cf2f1b 100644 --- a/src/Discord.Net/Common/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/IGuildChannel.cs @@ -20,9 +20,9 @@ namespace Discord /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// If true, a user accepting this invite will be kicked from the guild after closing their client. /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); + Task 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; } diff --git a/src/Discord.Net/Common/Entities/Channels/IMessageChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs similarity index 81% rename from src/Discord.Net/Common/Entities/Channels/IMessageChannel.cs rename to src/Discord.Net/Entities/Channels/IMessageChannel.cs index c2a10f30b..e0613da48 100644 --- a/src/Discord.Net/Common/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -6,8 +6,12 @@ namespace Discord { public interface IMessageChannel : IChannel { - /// Gets the message in this message channel with the given id, or null if none was found. - Task GetMessage(ulong id); + /// Gets all messages in this channel's cache. + IEnumerable 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. diff --git a/src/Discord.Net/Common/Entities/Channels/ITextChannel.cs b/src/Discord.Net/Entities/Channels/ITextChannel.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Channels/ITextChannel.cs rename to src/Discord.Net/Entities/Channels/ITextChannel.cs diff --git a/src/Discord.Net/Common/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs similarity index 70% rename from src/Discord.Net/Common/Entities/Channels/IVoiceChannel.cs rename to src/Discord.Net/Entities/Channels/IVoiceChannel.cs index baa2d741f..d94a97a63 100644 --- a/src/Discord.Net/Common/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net/Entities/Channels/IVoiceChannel.cs @@ -8,6 +8,8 @@ namespace Discord { /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. int Bitrate { get; } + /// Gets the max amount of users allowed to be connected to this channel at one time. A value of 0 represents no limit. + int UserLimit { get; } /// Modifies this voice channel. Task Modify(Action func); diff --git a/src/Discord.Net/Common/Entities/Guilds/Emoji.cs b/src/Discord.Net/Entities/Guilds/Emoji.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/Emoji.cs rename to src/Discord.Net/Entities/Guilds/Emoji.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs b/src/Discord.Net/Entities/Guilds/IGuild.cs similarity index 96% rename from src/Discord.Net/Common/Entities/Guilds/IGuild.cs rename to src/Discord.Net/Entities/Guilds/IGuild.cs index 9d6518612..2fc618196 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net/Entities/Guilds/IGuild.cs @@ -70,13 +70,13 @@ 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. /// If true, a user accepting this invite will be kicked from the guild after closing their client. /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); + Task 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(); diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuildEmbed.cs b/src/Discord.Net/Entities/Guilds/IGuildEmbed.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IGuildEmbed.cs rename to src/Discord.Net/Entities/Guilds/IGuildEmbed.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs similarity index 90% rename from src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs rename to src/Discord.Net/Entities/Guilds/IGuildIntegration.cs index 0252382fd..e90d8ae76 100644 --- a/src/Discord.Net/Common/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net/Entities/Guilds/IGuildIntegration.cs @@ -12,10 +12,10 @@ namespace Discord ulong ExpireBehavior { get; } ulong ExpireGracePeriod { get; } DateTime SyncedAt { get; } + IntegrationAccount Account { get; } IGuild Guild { get; } IUser User { get; } IRole Role { get; } - IIntegrationAccount Account { get; } } } diff --git a/src/Discord.Net/Common/Entities/Guilds/IUserGuild.cs b/src/Discord.Net/Entities/Guilds/IUserGuild.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IUserGuild.cs rename to src/Discord.Net/Entities/Guilds/IUserGuild.cs diff --git a/src/Discord.Net/Common/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net/Entities/Guilds/IVoiceRegion.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Guilds/IVoiceRegion.cs rename to src/Discord.Net/Entities/Guilds/IVoiceRegion.cs diff --git a/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs new file mode 100644 index 000000000..db0351bb1 --- /dev/null +++ b/src/Discord.Net/Entities/Guilds/IntegrationAccount.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct IntegrationAccount + { + /// + public string Id { get; } + + /// + public string Name { get; private set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs b/src/Discord.Net/Entities/Guilds/VoiceRegion.cs similarity index 71% rename from src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs rename to src/Discord.Net/Entities/Guilds/VoiceRegion.cs index 1c3ee7f20..126807202 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/VoiceRegion.cs +++ b/src/Discord.Net/Entities/Guilds/VoiceRegion.cs @@ -1,7 +1,9 @@ -using Model = Discord.API.VoiceRegion; +using System.Diagnostics; +using Model = Discord.API.VoiceRegion; -namespace Discord.Rest +namespace Discord { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class VoiceRegion : IVoiceRegion { /// @@ -27,6 +29,7 @@ namespace Discord.Rest SamplePort = model.SamplePort; } - public override string ToString() => $"{Name ?? Id.ToString()}"; + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsVip ? ", VIP" : "")}{(IsOptimal ? ", Optimal" : "")})"; } } diff --git a/src/Discord.Net/Common/Entities/IDeletable.cs b/src/Discord.Net/Entities/IDeletable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IDeletable.cs rename to src/Discord.Net/Entities/IDeletable.cs diff --git a/src/Discord.Net/Common/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IEntity.cs rename to src/Discord.Net/Entities/IEntity.cs diff --git a/src/Discord.Net/Common/Entities/IMentionable.cs b/src/Discord.Net/Entities/IMentionable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IMentionable.cs rename to src/Discord.Net/Entities/IMentionable.cs diff --git a/src/Discord.Net/Common/Entities/ISnowflakeEntity.cs b/src/Discord.Net/Entities/ISnowflakeEntity.cs similarity index 100% rename from src/Discord.Net/Common/Entities/ISnowflakeEntity.cs rename to src/Discord.Net/Entities/ISnowflakeEntity.cs diff --git a/src/Discord.Net/Common/Entities/IUpdateable.cs b/src/Discord.Net/Entities/IUpdateable.cs similarity index 100% rename from src/Discord.Net/Common/Entities/IUpdateable.cs rename to src/Discord.Net/Entities/IUpdateable.cs diff --git a/src/Discord.Net/Common/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs similarity index 93% rename from src/Discord.Net/Common/Entities/Invites/IInvite.cs rename to src/Discord.Net/Entities/Invites/IInvite.cs index d0eb8888c..4b0f55f59 100644 --- a/src/Discord.Net/Common/Entities/Invites/IInvite.cs +++ b/src/Discord.Net/Entities/Invites/IInvite.cs @@ -2,7 +2,7 @@ namespace Discord { - public interface IInvite : IEntity + public interface IInvite : IEntity, IDeletable { /// Gets the unique identifier for this invite. string Code { get; } diff --git a/src/Discord.Net/Common/Entities/Invites/IGuildInvite.cs b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs similarity index 81% rename from src/Discord.Net/Common/Entities/Invites/IGuildInvite.cs rename to src/Discord.Net/Entities/Invites/IInviteMetadata.cs index 25e6d0e80..a2e18a2e7 100644 --- a/src/Discord.Net/Common/Entities/Invites/IGuildInvite.cs +++ b/src/Discord.Net/Entities/Invites/IInviteMetadata.cs @@ -1,6 +1,6 @@ namespace Discord { - public interface IGuildInvite : IDeletable, IInvite + public interface IInviteMetadata : IInvite { /// Returns true if this invite was revoked. bool IsRevoked { get; } @@ -12,8 +12,5 @@ int? MaxUses { get; } /// Gets the amount of times this invite has been used. int Uses { get; } - - /// Gets the guild this invite is linked to. - IGuild Guild { get; } } } \ No newline at end of file diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs new file mode 100644 index 000000000..97e1fc051 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -0,0 +1,65 @@ +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Invite; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Invite : IInvite + { + /// + public string Code { get; } + internal IDiscordClient Discord { get; } + + /// + 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 string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; + /// + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; + + + internal Invite(IDiscordClient discord, Model model) + { + Discord = discord; + Code = model.Code; + + Update(model); + } + protected virtual void Update(Model model) + { + XkcdCode = model.XkcdPass; + GuildId = model.Guild.Id; + ChannelId = model.Channel.Id; + GuildName = model.Guild.Name; + 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 new file mode 100644 index 000000000..61f353ebd --- /dev/null +++ b/src/Discord.Net/Entities/Invites/InviteMetadata.cs @@ -0,0 +1,32 @@ +using Model = Discord.API.InviteMetadata; + +namespace Discord +{ + public 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) + : base(client, model) + { + Update(model); + } + private void Update(Model model) + { + IsRevoked = model.Revoked; + IsTemporary = model.Temporary; + MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; + MaxUses = model.MaxUses; + Uses = model.Uses; + } + } +} diff --git a/src/Discord.Net/Common/Entities/Messages/Attachment.cs b/src/Discord.Net/Entities/Messages/Attachment.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Attachment.cs rename to src/Discord.Net/Entities/Messages/Attachment.cs diff --git a/src/Discord.Net/Common/Entities/Messages/Direction.cs b/src/Discord.Net/Entities/Messages/Direction.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Direction.cs rename to src/Discord.Net/Entities/Messages/Direction.cs diff --git a/src/Discord.Net/Common/Entities/Messages/Embed.cs b/src/Discord.Net/Entities/Messages/Embed.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/Embed.cs rename to src/Discord.Net/Entities/Messages/Embed.cs diff --git a/src/Discord.Net/Common/Entities/Messages/EmbedProvider.cs b/src/Discord.Net/Entities/Messages/EmbedProvider.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/EmbedProvider.cs rename to src/Discord.Net/Entities/Messages/EmbedProvider.cs diff --git a/src/Discord.Net/Common/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net/Entities/Messages/EmbedThumbnail.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Messages/EmbedThumbnail.cs rename to src/Discord.Net/Entities/Messages/EmbedThumbnail.cs diff --git a/src/Discord.Net/Common/Entities/Messages/IMessage.cs b/src/Discord.Net/Entities/Messages/IMessage.cs similarity index 99% rename from src/Discord.Net/Common/Entities/Messages/IMessage.cs rename to src/Discord.Net/Entities/Messages/IMessage.cs index 1e5eb3b3f..35107ccf2 100644 --- a/src/Discord.Net/Common/Entities/Messages/IMessage.cs +++ b/src/Discord.Net/Entities/Messages/IMessage.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Discord.API.Rest; +using System.Collections.Generic; namespace Discord { diff --git a/src/Discord.Net/Common/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net/Entities/Permissions/ChannelPermission.cs similarity index 94% rename from src/Discord.Net/Common/Entities/Permissions/ChannelPermission.cs rename to src/Discord.Net/Entities/Permissions/ChannelPermission.cs index df8b88480..f3592b010 100644 --- a/src/Discord.Net/Common/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net/Entities/Permissions/ChannelPermission.cs @@ -1,6 +1,6 @@ namespace Discord { - internal enum ChannelPermission : byte + public enum ChannelPermission : byte { //General CreateInstantInvite = 0, diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs new file mode 100644 index 000000000..f5760f1a9 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct ChannelPermissions + { +#if CSHARP7 + private static ChannelPermissions _allDM { get; } = new ChannelPermissions(0b000100_000000_0011111111_0000011001); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(0b000000_000000_0001110011_0000000000); + private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(0b000100_111111_0000000000_0000011001); +#else + private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000000111111110000011001", 2)); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000000011100110000000000", 2)); + private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000011001", 2)); +#endif + + /// Gets a blank ChannelPermissions that grants no permissions. + public static ChannelPermissions None { get; } = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for a given channelType. + public static ChannelPermissions All(IChannel channel) + { +#if CSHARP7 + switch (channel) + { + case ITextChannel _: return _allText; + case IVoiceChannel _: return _allVoice; + case IDMChannel _: return _allDM; + default: + throw new ArgumentException("Unknown channel type", nameof(channel)); + } +#else + if (channel is ITextChannel) return _allText; + if (channel is IVoiceChannel) return _allVoice; + if (channel is IDMChannel) return _allDM; + + throw new ArgumentException("Unknown channel type", nameof(channel)); +#endif + } + + /// Gets a packed value representing all the permissions in this ChannelPermissions. + public ulong RawValue { get; } + + /// If True, a user may create invites. + public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); + /// If True, a user may create, delete and modify this channel. + public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannel); + + /// If True, a user may join channels. + public bool ReadMessages => Permissions.GetValue(RawValue, ChannelPermission.ReadMessages); + /// If True, a user may send messages. + public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); + /// If True, a user may send text-to-speech messages. + public bool SendTTSMessages => Permissions.GetValue(RawValue, ChannelPermission.SendTTSMessages); + /// If True, a user may delete messages. + public bool ManageMessages => Permissions.GetValue(RawValue, ChannelPermission.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public bool EmbedLinks => Permissions.GetValue(RawValue, ChannelPermission.EmbedLinks); + /// If True, a user may send files. + public bool AttachFiles => Permissions.GetValue(RawValue, ChannelPermission.AttachFiles); + /// If True, a user may read previous messages. + public bool ReadMessageHistory => Permissions.GetValue(RawValue, ChannelPermission.ReadMessageHistory); + /// If True, a user may mention @everyone. + public bool MentionEveryone => Permissions.GetValue(RawValue, ChannelPermission.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public bool Connect => Permissions.GetValue(RawValue, ChannelPermission.Connect); + /// If True, a user may speak in a voice channel. + public bool Speak => Permissions.GetValue(RawValue, ChannelPermission.Speak); + /// If True, a user may mute users. + public bool MuteMembers => Permissions.GetValue(RawValue, ChannelPermission.MuteMembers); + /// If True, a user may deafen users. + public bool DeafenMembers => Permissions.GetValue(RawValue, ChannelPermission.DeafenMembers); + /// If True, a user may move other users between voice channels. + public bool MoveMembers => Permissions.GetValue(RawValue, ChannelPermission.MoveMembers); + /// If True, a user may use voice-activity-detection rather than push-to-talk. + public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); + + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public bool ManagePermissions => Permissions.GetValue(RawValue, ChannelPermission.ManagePermissions); + + /// Creates a new ChannelPermissions with the provided packed value. + public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } + + private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) + { + ulong value = initialValue; + + Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); + Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); + Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); + Permissions.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); + Permissions.SetValue(ref value, embedLinks, ChannelPermission.EmbedLinks); + Permissions.SetValue(ref value, attachFiles, ChannelPermission.AttachFiles); + Permissions.SetValue(ref value, readMessageHistory, ChannelPermission.ReadMessageHistory); + Permissions.SetValue(ref value, mentionEveryone, ChannelPermission.MentionEveryone); + Permissions.SetValue(ref value, connect, ChannelPermission.Connect); + Permissions.SetValue(ref value, speak, ChannelPermission.Speak); + Permissions.SetValue(ref value, muteMembers, ChannelPermission.MuteMembers); + Permissions.SetValue(ref value, deafenMembers, ChannelPermission.DeafenMembers); + Permissions.SetValue(ref value, moveMembers, ChannelPermission.MoveMembers); + Permissions.SetValue(ref value, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref value, managePermissions, ChannelPermission.ManagePermissions); + + RawValue = value; + } + + /// Creates a new ChannelPermissions with the provided permissions. + public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, + bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, + bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, + bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false) + : this(0, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation, managePermissions) { } + + /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null) + => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation, managePermissions); + + public List ToList() + { + var perms = new List(); + ulong x = 1; + for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + { + if ((RawValue & x) != 0) + perms.Add((ChannelPermission)i); + } + return perms; + } + /// + public override string ToString() => RawValue.ToString(); + private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; + } +} diff --git a/src/Discord.Net/Common/Entities/Permissions/GuildPermission.cs b/src/Discord.Net/Entities/Permissions/GuildPermission.cs similarity index 94% rename from src/Discord.Net/Common/Entities/Permissions/GuildPermission.cs rename to src/Discord.Net/Entities/Permissions/GuildPermission.cs index 497b78726..3ba869cc5 100644 --- a/src/Discord.Net/Common/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net/Entities/Permissions/GuildPermission.cs @@ -1,6 +1,6 @@ namespace Discord { - internal enum GuildPermission : byte + public enum GuildPermission : byte { //General CreateInstantInvite = 0, diff --git a/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs similarity index 52% rename from src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs rename to src/Discord.Net/Entities/Permissions/GuildPermissions.cs index 55b62599e..899cac80a 100644 --- a/src/Discord.Net/Common/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -1,71 +1,78 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; namespace Discord { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct GuildPermissions { /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); /// Gets a GuildPermissions that grants all permissions. +#if CSHARP7 public static readonly GuildPermissions All = new GuildPermissions(0b000111_111111_0011111111_0000111111); +#else + public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("00011111111100111111110000111111", 2)); +#endif /// Gets a packed value representing all the permissions in this GuildPermissions. - public uint RawValue { get; } + public ulong RawValue { get; } /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionUtilities.GetValue(RawValue, GuildPermission.CreateInstantInvite); + public bool CreateInstantInvite => Permissions.GetValue(RawValue, GuildPermission.CreateInstantInvite); /// If True, a user may ban users from the guild. - public bool BanMembers => PermissionUtilities.GetValue(RawValue, GuildPermission.BanMembers); + public bool BanMembers => Permissions.GetValue(RawValue, GuildPermission.BanMembers); /// If True, a user may kick users from the guild. - public bool KickMembers => PermissionUtilities.GetValue(RawValue, GuildPermission.KickMembers); + public bool KickMembers => Permissions.GetValue(RawValue, GuildPermission.KickMembers); /// If True, a user is granted all permissions, and cannot have them revoked via channel permissions. - public bool Administrator => PermissionUtilities.GetValue(RawValue, GuildPermission.Administrator); + public bool Administrator => Permissions.GetValue(RawValue, GuildPermission.Administrator); /// If True, a user may create, delete and modify channels. - public bool ManageChannels => PermissionUtilities.GetValue(RawValue, GuildPermission.ManageChannels); + public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); /// If True, a user may adjust guild properties. - public bool ManageGuild => PermissionUtilities.GetValue(RawValue, GuildPermission.ManageGuild); + public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); /// If True, a user may join channels. - public bool ReadMessages => PermissionUtilities.GetValue(RawValue, GuildPermission.ReadMessages); + public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); /// If True, a user may send messages. - public bool SendMessages => PermissionUtilities.GetValue(RawValue, GuildPermission.SendMessages); + public bool SendMessages => Permissions.GetValue(RawValue, GuildPermission.SendMessages); /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionUtilities.GetValue(RawValue, GuildPermission.SendTTSMessages); + public bool SendTTSMessages => Permissions.GetValue(RawValue, GuildPermission.SendTTSMessages); /// If True, a user may delete messages. - public bool ManageMessages => PermissionUtilities.GetValue(RawValue, GuildPermission.ManageMessages); + public bool ManageMessages => Permissions.GetValue(RawValue, GuildPermission.ManageMessages); /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionUtilities.GetValue(RawValue, GuildPermission.EmbedLinks); + public bool EmbedLinks => Permissions.GetValue(RawValue, GuildPermission.EmbedLinks); /// If True, a user may send files. - public bool AttachFiles => PermissionUtilities.GetValue(RawValue, GuildPermission.AttachFiles); + public bool AttachFiles => Permissions.GetValue(RawValue, GuildPermission.AttachFiles); /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionUtilities.GetValue(RawValue, GuildPermission.ReadMessageHistory); + public bool ReadMessageHistory => Permissions.GetValue(RawValue, GuildPermission.ReadMessageHistory); /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionUtilities.GetValue(RawValue, GuildPermission.MentionEveryone); + public bool MentionEveryone => Permissions.GetValue(RawValue, GuildPermission.MentionEveryone); /// If True, a user may connect to a voice channel. - public bool Connect => PermissionUtilities.GetValue(RawValue, GuildPermission.Connect); + public bool Connect => Permissions.GetValue(RawValue, GuildPermission.Connect); /// If True, a user may speak in a voice channel. - public bool Speak => PermissionUtilities.GetValue(RawValue, GuildPermission.Speak); + public bool Speak => Permissions.GetValue(RawValue, GuildPermission.Speak); /// If True, a user may mute users. - public bool MuteMembers => PermissionUtilities.GetValue(RawValue, GuildPermission.MuteMembers); + public bool MuteMembers => Permissions.GetValue(RawValue, GuildPermission.MuteMembers); /// If True, a user may deafen users. - public bool DeafenMembers => PermissionUtilities.GetValue(RawValue, GuildPermission.DeafenMembers); + public bool DeafenMembers => Permissions.GetValue(RawValue, GuildPermission.DeafenMembers); /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionUtilities.GetValue(RawValue, GuildPermission.MoveMembers); + public bool MoveMembers => Permissions.GetValue(RawValue, GuildPermission.MoveMembers); /// If True, a user may use voice-activity-detection rather than push-to-talk. - public bool UseVAD => PermissionUtilities.GetValue(RawValue, GuildPermission.UseVAD); + public bool UseVAD => Permissions.GetValue(RawValue, GuildPermission.UseVAD); /// If True, a user may change their own nickname. - public bool ChangeNickname => PermissionUtilities.GetValue(RawValue, GuildPermission.ChangeNickname); + public bool ChangeNickname => Permissions.GetValue(RawValue, GuildPermission.ChangeNickname); /// If True, a user may change the nickname of other users. - public bool ManageNicknames => PermissionUtilities.GetValue(RawValue, GuildPermission.ManageNicknames); + public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); /// If True, a user may adjust roles. - public bool ManageRoles => PermissionUtilities.GetValue(RawValue, GuildPermission.ManageRoles); + public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); /// Creates a new GuildPermissions with the provided packed value. - public GuildPermissions(uint rawValue) { RawValue = rawValue; } + public GuildPermissions(ulong rawValue) { RawValue = rawValue; } - private GuildPermissions(uint initialValue, bool? createInstantInvite = null, bool? kickMembers = null, + private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, bool? banMembers = null, bool? administrator = null, bool? manageChannel = null, bool? manageGuild = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, @@ -73,31 +80,31 @@ namespace Discord bool? moveMembers = null, bool? useVoiceActivation = null, bool? changeNickname = null, bool? manageNicknames = null, bool? manageRoles = null) { - uint value = initialValue; + ulong value = initialValue; - PermissionUtilities.SetValue(ref value, createInstantInvite, GuildPermission.CreateInstantInvite); - PermissionUtilities.SetValue(ref value, banMembers, GuildPermission.BanMembers); - PermissionUtilities.SetValue(ref value, kickMembers, GuildPermission.KickMembers); - PermissionUtilities.SetValue(ref value, administrator, GuildPermission.Administrator); - PermissionUtilities.SetValue(ref value, manageChannel, GuildPermission.ManageChannels); - PermissionUtilities.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); - PermissionUtilities.SetValue(ref value, readMessages, GuildPermission.ReadMessages); - PermissionUtilities.SetValue(ref value, sendMessages, GuildPermission.SendMessages); - PermissionUtilities.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); - PermissionUtilities.SetValue(ref value, manageMessages, GuildPermission.ManageMessages); - PermissionUtilities.SetValue(ref value, embedLinks, GuildPermission.EmbedLinks); - PermissionUtilities.SetValue(ref value, attachFiles, GuildPermission.AttachFiles); - PermissionUtilities.SetValue(ref value, readMessageHistory, GuildPermission.ReadMessageHistory); - PermissionUtilities.SetValue(ref value, mentionEveryone, GuildPermission.MentionEveryone); - PermissionUtilities.SetValue(ref value, connect, GuildPermission.Connect); - PermissionUtilities.SetValue(ref value, speak, GuildPermission.Speak); - PermissionUtilities.SetValue(ref value, muteMembers, GuildPermission.MuteMembers); - PermissionUtilities.SetValue(ref value, deafenMembers, GuildPermission.DeafenMembers); - PermissionUtilities.SetValue(ref value, moveMembers, GuildPermission.MoveMembers); - PermissionUtilities.SetValue(ref value, useVoiceActivation, GuildPermission.UseVAD); - PermissionUtilities.SetValue(ref value, changeNickname, GuildPermission.ChangeNickname); - PermissionUtilities.SetValue(ref value, manageNicknames, GuildPermission.ManageNicknames); - PermissionUtilities.SetValue(ref value, manageRoles, GuildPermission.ManageRoles); + Permissions.SetValue(ref value, createInstantInvite, GuildPermission.CreateInstantInvite); + Permissions.SetValue(ref value, banMembers, GuildPermission.BanMembers); + Permissions.SetValue(ref value, kickMembers, GuildPermission.KickMembers); + Permissions.SetValue(ref value, administrator, GuildPermission.Administrator); + Permissions.SetValue(ref value, manageChannel, GuildPermission.ManageChannels); + Permissions.SetValue(ref value, manageGuild, GuildPermission.ManageGuild); + Permissions.SetValue(ref value, readMessages, GuildPermission.ReadMessages); + Permissions.SetValue(ref value, sendMessages, GuildPermission.SendMessages); + Permissions.SetValue(ref value, sendTTSMessages, GuildPermission.SendTTSMessages); + Permissions.SetValue(ref value, manageMessages, GuildPermission.ManageMessages); + Permissions.SetValue(ref value, embedLinks, GuildPermission.EmbedLinks); + Permissions.SetValue(ref value, attachFiles, GuildPermission.AttachFiles); + Permissions.SetValue(ref value, readMessageHistory, GuildPermission.ReadMessageHistory); + Permissions.SetValue(ref value, mentionEveryone, GuildPermission.MentionEveryone); + Permissions.SetValue(ref value, connect, GuildPermission.Connect); + Permissions.SetValue(ref value, speak, GuildPermission.Speak); + Permissions.SetValue(ref value, muteMembers, GuildPermission.MuteMembers); + Permissions.SetValue(ref value, deafenMembers, GuildPermission.DeafenMembers); + Permissions.SetValue(ref value, moveMembers, GuildPermission.MoveMembers); + Permissions.SetValue(ref value, useVoiceActivation, GuildPermission.UseVAD); + Permissions.SetValue(ref value, changeNickname, GuildPermission.ChangeNickname); + Permissions.SetValue(ref value, manageNicknames, GuildPermission.ManageNicknames); + Permissions.SetValue(ref value, manageRoles, GuildPermission.ManageRoles); RawValue = value; } @@ -125,21 +132,20 @@ namespace Discord => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannels, manageGuild, readMessages, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, changeNickname, manageNicknames, manageRoles); - - /// - public override string ToString() + + public List ToList() { - var perms = new List(); - int x = 1; - for (byte i = 0; i < 32; i++, x <<= 1) + var perms = new List(); + ulong x = 1; + for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) { if ((RawValue & x) != 0) - { - if (System.Enum.IsDefined(typeof(GuildPermission), i)) - perms.Add($"{(GuildPermission)i}"); - } + perms.Add((GuildPermission)i); } - return string.Join(", ", perms); + return perms; } + /// + public override string ToString() => RawValue.ToString(); + private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; } } diff --git a/src/Discord.Net/Common/Entities/Permissions/Overwrite.cs b/src/Discord.Net/Entities/Permissions/Overwrite.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/Overwrite.cs rename to src/Discord.Net/Entities/Permissions/Overwrite.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs similarity index 52% rename from src/Discord.Net/Common/Entities/Permissions/OverwritePermissions.cs rename to src/Discord.Net/Entities/Permissions/OverwritePermissions.cs index 735b17dcf..7c448522e 100644 --- a/src/Discord.Net/Common/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Discord { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct OverwritePermissions { /// Gets a blank OverwritePermissions that inherits all permissions. @@ -15,77 +17,77 @@ namespace Discord => new OverwritePermissions(0, ChannelPermissions.All(channel).RawValue); /// Gets a packed value representing all the allowed permissions in this OverwritePermissions. - public uint AllowValue { get; } + public ulong AllowValue { get; } /// Gets a packed value representing all the denied permissions in this OverwritePermissions. - public uint DenyValue { get; } + public ulong DenyValue { get; } /// If True, a user may create invites. - public PermValue CreateInstantInvite => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.CreateInstantInvite); + public PermValue CreateInstantInvite => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.CreateInstantInvite); /// If True, a user may create, delete and modify this channel. - public PermValue ManageChannel => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannel); + public PermValue ManageChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageChannel); /// If True, a user may join channels. - public PermValue ReadMessages => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessages); + public PermValue ReadMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessages); /// If True, a user may send messages. - public PermValue SendMessages => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); + public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); /// If True, a user may send text-to-speech messages. - public PermValue SendTTSMessages => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.SendTTSMessages); + public PermValue SendTTSMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendTTSMessages); /// If True, a user may delete messages. - public PermValue ManageMessages => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.ManageMessages); + public PermValue ManageMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageMessages); /// If True, Discord will auto-embed links sent by this user. - public PermValue EmbedLinks => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.EmbedLinks); + public PermValue EmbedLinks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.EmbedLinks); /// If True, a user may send files. - public PermValue AttachFiles => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.AttachFiles); + public PermValue AttachFiles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AttachFiles); /// If True, a user may read previous messages. - public PermValue ReadMessageHistory => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessageHistory); + public PermValue ReadMessageHistory => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessageHistory); /// If True, a user may mention @everyone. - public PermValue MentionEveryone => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.MentionEveryone); + public PermValue MentionEveryone => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MentionEveryone); /// If True, a user may connect to a voice channel. - public PermValue Connect => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.Connect); + public PermValue Connect => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Connect); /// If True, a user may speak in a voice channel. - public PermValue Speak => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.Speak); + public PermValue Speak => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Speak); /// If True, a user may mute users. - public PermValue MuteMembers => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.MuteMembers); + public PermValue MuteMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MuteMembers); /// If True, a user may deafen users. - public PermValue DeafenMembers => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.DeafenMembers); + public PermValue DeafenMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.DeafenMembers); /// If True, a user may move other users between voice channels. - public PermValue MoveMembers => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); + public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); /// If True, a user may use voice-activity-detection rather than push-to-talk. - public PermValue UseVAD => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); + public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public PermValue ManagePermissions => PermissionUtilities.GetValue(AllowValue, DenyValue, ChannelPermission.ManagePermissions); + public PermValue ManagePermissions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManagePermissions); /// Creates a new OverwritePermissions with the provided allow and deny packed values. - public OverwritePermissions(uint allowValue, uint denyValue) + public OverwritePermissions(ulong allowValue, ulong denyValue) { AllowValue = allowValue; DenyValue = denyValue; } - private OverwritePermissions(uint allowValue, uint denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, + private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? managePermissions = null) { - PermissionUtilities.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannel); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, manageMessages, ChannelPermission.ManageMessages); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, embedLinks, ChannelPermission.EmbedLinks); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, attachFiles, ChannelPermission.AttachFiles); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, readMessageHistory, ChannelPermission.ReadMessageHistory); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, mentionEveryone, ChannelPermission.MentionEveryone); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, connect, ChannelPermission.Connect); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, speak, ChannelPermission.Speak); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, muteMembers, ChannelPermission.MuteMembers); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); - PermissionUtilities.SetValue(ref allowValue, ref denyValue, managePermissions, ChannelPermission.ManagePermissions); + Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); + Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannel); + Permissions.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref allowValue, ref denyValue, sendMessages, ChannelPermission.SendMessages); + Permissions.SetValue(ref allowValue, ref denyValue, sendTTSMessages, ChannelPermission.SendTTSMessages); + Permissions.SetValue(ref allowValue, ref denyValue, manageMessages, ChannelPermission.ManageMessages); + Permissions.SetValue(ref allowValue, ref denyValue, embedLinks, ChannelPermission.EmbedLinks); + Permissions.SetValue(ref allowValue, ref denyValue, attachFiles, ChannelPermission.AttachFiles); + Permissions.SetValue(ref allowValue, ref denyValue, readMessageHistory, ChannelPermission.ReadMessageHistory); + Permissions.SetValue(ref allowValue, ref denyValue, mentionEveryone, ChannelPermission.MentionEveryone); + Permissions.SetValue(ref allowValue, ref denyValue, connect, ChannelPermission.Connect); + Permissions.SetValue(ref allowValue, ref denyValue, speak, ChannelPermission.Speak); + Permissions.SetValue(ref allowValue, ref denyValue, muteMembers, ChannelPermission.MuteMembers); + Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); + Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); + Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref allowValue, ref denyValue, managePermissions, ChannelPermission.ManagePermissions); AllowValue = allowValue; DenyValue = denyValue; @@ -111,25 +113,32 @@ namespace Discord embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions); - /// - public override string ToString() + public List ToAllowList() { - var perms = new List(); - int x = 1; - for (byte i = 0; i < 32; i++, x <<= 1) + var perms = new List(); + ulong x = 1; + for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) { if ((AllowValue & x) != 0) - { - if (Enum.IsDefined(typeof(GuildPermission), i)) - perms.Add($"+{(GuildPermission)i}"); - } - else if ((DenyValue & x) != 0) - { - if (Enum.IsDefined(typeof(GuildPermission), i)) - perms.Add($"-{(GuildPermission)i}"); - } + perms.Add((ChannelPermission)i); + } + return perms; + } + public List ToDenyList() + { + var perms = new List(); + ulong x = 1; + for (byte i = 0; i < Permissions.MaxBits; i++, x <<= 1) + { + if ((DenyValue & x) != 0) + perms.Add((ChannelPermission)i); } - return string.Join(", ", perms); + return perms; } + /// + public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; + private string DebuggerDisplay => + $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + + $"Deny {DenyValue} ({string.Join(", ", ToDenyList())})"; } } diff --git a/src/Discord.Net/Common/Entities/Permissions/PermValue.cs b/src/Discord.Net/Entities/Permissions/PermValue.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/PermValue.cs rename to src/Discord.Net/Entities/Permissions/PermValue.cs diff --git a/src/Discord.Net/Common/Entities/Permissions/PermissionTarget.cs b/src/Discord.Net/Entities/Permissions/PermissionTarget.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Permissions/PermissionTarget.cs rename to src/Discord.Net/Entities/Permissions/PermissionTarget.cs diff --git a/src/Discord.Net/Entities/Permissions/Permissions.cs b/src/Discord.Net/Entities/Permissions/Permissions.cs new file mode 100644 index 000000000..3cd17e66e --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/Permissions.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; + +namespace Discord +{ + internal static class Permissions + { + public const int MaxBits = 53; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, ChannelPermission bit) + => GetValue(allow, deny, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, GuildPermission bit) + => GetValue(allow, deny, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(ulong allow, ulong deny, byte bit) + { + if (HasBit(allow, bit)) + return PermValue.Allow; + else if (HasBit(deny, bit)) + return PermValue.Deny; + else + return PermValue.Inherit; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, ChannelPermission bit) + => GetValue(value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, GuildPermission bit) + => GetValue(value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(ulong value, byte bit) => HasBit(value, bit); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, ChannelPermission bit) + => SetValue(ref rawValue, value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, GuildPermission bit) + => SetValue(ref rawValue, value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong rawValue, bool? value, byte bit) + { + if (value.HasValue) + { + if (value == true) + SetBit(ref rawValue, bit); + else + UnsetBit(ref rawValue, bit); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, ChannelPermission bit) + => SetValue(ref allow, ref deny, value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, GuildPermission bit) + => SetValue(ref allow, ref deny, value, (byte)bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, byte bit) + { + if (value.HasValue) + { + switch (value) + { + case PermValue.Allow: + SetBit(ref allow, bit); + UnsetBit(ref deny, bit); + break; + case PermValue.Deny: + UnsetBit(ref allow, bit); + SetBit(ref deny, bit); + break; + default: + UnsetBit(ref allow, bit); + UnsetBit(ref deny, bit); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasBit(ulong value, byte bit) => (value & (1U << bit)) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetBit(ref ulong value, byte bit) => value |= (1U << bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); + + public static ulong ResolveGuild(IGuildUser user) + { + var roles = user.Roles; + ulong newPermissions = 0; + for (int i = 0; i < roles.Count; i++) + newPermissions |= roles[i].Permissions.RawValue; + return newPermissions; + } + + /*public static ulong ResolveChannel(IGuildUser user, IGuildChannel channel) + { + return ResolveChannel(user, channel, ResolveGuild(user)); + }*/ + public static ulong ResolveChannel(IGuildUser user, IGuildChannel channel, ulong guildPermissions) + { + ulong resolvedPermissions = 0; + + ulong mask = ChannelPermissions.All(channel).RawValue; + if (user.Id == user.Guild.OwnerId || GetValue(resolvedPermissions, GuildPermission.Administrator)) + resolvedPermissions = mask; //Owners and administrators always have all permissions + else + { + //Start with this user's guild permissions + resolvedPermissions = guildPermissions; + var overwrites = channel.PermissionOverwrites; + + Overwrite entry; + var roles = user.Roles; + if (roles.Count > 0) + { + for (int i = 0; i < roles.Count; i++) + { + 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; + } + } + if (overwrites.TryGetValue(user.Id, out entry)) + resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; + +#if CSHARP7 + switch (channel) + { + case ITextChannel _: + if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + resolvedPermissions = 0; //No read permission on a text channel removes all other permissions + break; + case IVoiceChannel _: + if (!GetValue(resolvedPermissions, ChannelPermission.Connect)) + resolvedPermissions = 0; //No read permission on a text channel removes all other permissions + break; + } +#else + var textChannel = channel as ITextChannel; + var voiceChannel = channel as IVoiceChannel; + if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + resolvedPermissions = 0; //No read permission on a text channel removes all other permissions + else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) + resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions +#endif + resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) + } + + return resolvedPermissions; + } + } +} diff --git a/src/Discord.Net/Common/Entities/Roles/Color.cs b/src/Discord.Net/Entities/Roles/Color.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Roles/Color.cs rename to src/Discord.Net/Entities/Roles/Color.cs diff --git a/src/Discord.Net/Common/Entities/Roles/IRole.cs b/src/Discord.Net/Entities/Roles/IRole.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Roles/IRole.cs rename to src/Discord.Net/Entities/Roles/IRole.cs diff --git a/src/Discord.Net/Entities/Users/Connection.cs b/src/Discord.Net/Entities/Users/Connection.cs new file mode 100644 index 000000000..10852820e --- /dev/null +++ b/src/Discord.Net/Entities/Users/Connection.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Model = Discord.API.Connection; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Connection : IConnection + { + public string Id { get; } + public string Type { get; } + public string Name { get; } + public bool IsRevoked { get; } + + public IEnumerable IntegrationIds { get; } + + public Connection(Model model) + { + Id = model.Id; + + Type = model.Type; + Name = model.Name; + IsRevoked = model.Revoked; + + IntegrationIds = model.Integrations; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Type = {Type}{(IsRevoked ? ", Revoked" : "")})"; + } +} diff --git a/src/Discord.Net/Entities/Users/Game.cs b/src/Discord.Net/Entities/Users/Game.cs new file mode 100644 index 000000000..ee5559165 --- /dev/null +++ b/src/Discord.Net/Entities/Users/Game.cs @@ -0,0 +1,22 @@ +namespace Discord +{ + public struct Game + { + public string Name { get; } + 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; + } + } +} diff --git a/src/Discord.Net/Common/Entities/Users/IConnection.cs b/src/Discord.Net/Entities/Users/IConnection.cs similarity index 81% rename from src/Discord.Net/Common/Entities/Users/IConnection.cs rename to src/Discord.Net/Entities/Users/IConnection.cs index 3c9b5a79e..6540c147e 100644 --- a/src/Discord.Net/Common/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 Integrations { get; } + IEnumerable IntegrationIds { get; } } } diff --git a/src/Discord.Net/Common/Entities/Users/IGuildUser.cs b/src/Discord.Net/Entities/Users/IGuildUser.cs similarity index 85% rename from src/Discord.Net/Common/Entities/Users/IGuildUser.cs rename to src/Discord.Net/Entities/Users/IGuildUser.cs index 222530ecd..f5d17688c 100644 --- a/src/Discord.Net/Common/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net/Entities/Users/IGuildUser.cs @@ -21,17 +21,14 @@ namespace Discord IGuild Guild { get; } /// Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. IReadOnlyList Roles { get; } - /// Gets the id of the voice channel this user is currently in, if any. - ulong? VoiceChannelId { 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); - /// Return true if this user has the provided role. - bool HasRole(IRole role); - /// Kicks this user from this guild. Task Kick(); /// Modifies this user's properties in this guild. diff --git a/src/Discord.Net/Common/Entities/Users/ISelfUser.cs b/src/Discord.Net/Entities/Users/ISelfUser.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/ISelfUser.cs rename to src/Discord.Net/Entities/Users/ISelfUser.cs diff --git a/src/Discord.Net/Common/Entities/Users/IUser.cs b/src/Discord.Net/Entities/Users/IUser.cs similarity index 84% rename from src/Discord.Net/Common/Entities/Users/IUser.cs rename to src/Discord.Net/Entities/Users/IUser.cs index 550c53e8b..c4754f3e3 100644 --- a/src/Discord.Net/Common/Entities/Users/IUser.cs +++ b/src/Discord.Net/Entities/Users/IUser.cs @@ -7,7 +7,7 @@ namespace Discord /// Gets the url to this user's avatar. string AvatarUrl { get; } /// Gets the game this user is currently playing, if any. - string CurrentGame { get; } + Game? CurrentGame { get; } /// Gets the per-username unique id for this user. ushort Discriminator { get; } /// Returns true if this user is a bot account. @@ -17,6 +17,7 @@ namespace Discord /// Gets the username for this user. string Username { get; } + //TODO: CreateDMChannel is a candidate to move to IGuildUser, and User made a common class, depending on next friends list update /// Returns a private message channel to this user, creating one if it does not already exist. Task CreateDMChannel(); } diff --git a/src/Discord.Net/Common/Entities/Users/IVoiceState.cs.old b/src/Discord.Net/Entities/Users/IVoiceState.cs.old similarity index 100% rename from src/Discord.Net/Common/Entities/Users/IVoiceState.cs.old rename to src/Discord.Net/Entities/Users/IVoiceState.cs.old diff --git a/src/Discord.Net/Entities/Users/StreamType.cs b/src/Discord.Net/Entities/Users/StreamType.cs new file mode 100644 index 000000000..7622e3d6e --- /dev/null +++ b/src/Discord.Net/Entities/Users/StreamType.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + public enum StreamType + { + NotStreaming = 0, + Twitch = 1 + } +} diff --git a/src/Discord.Net/Common/Entities/Users/UserStatus.cs b/src/Discord.Net/Entities/Users/UserStatus.cs similarity index 100% rename from src/Discord.Net/Common/Entities/Users/UserStatus.cs rename to src/Discord.Net/Entities/Users/UserStatus.cs diff --git a/src/Discord.Net/EventExtensions.cs b/src/Discord.Net/EventExtensions.cs new file mode 100644 index 000000000..b46cb9056 --- /dev/null +++ b/src/Discord.Net/EventExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class EventExtensions + { + //TODO: Optimize these for if there is only 1 subscriber (can we do this?) + public static async Task Raise(this Func eventHandler) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke().ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T arg) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg).ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2).ConfigureAwait(false); + } + } + public static async Task Raise(this Func eventHandler, T1 arg1, T2 arg2, T3 arg3) + { + var subscriptions = eventHandler?.GetInvocationList(); + if (subscriptions != null) + { + for (int i = 0; i < subscriptions.Length; i++) + await (subscriptions[i] as Func).Invoke(arg1, arg2, arg3).ConfigureAwait(false); + } + } + } +} diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index 4d12632cd..21c3c477c 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,5 +1,6 @@ using Discord.API; -using Discord.Net.Rest; +using Discord.Net.Queue; +using Discord.WebSocket.Data; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -9,15 +10,20 @@ namespace Discord //TODO: Add docstrings public interface IDiscordClient { - TokenType AuthTokenType { get; } - DiscordRawClient BaseClient { get; } - IRestClient RestClient { get; } + LoginState LoginState { get; } + 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(); + Task Connect(); + Task Disconnect(); + Task GetChannel(ulong id); Task> GetDMChannels(); @@ -27,7 +33,7 @@ namespace Discord Task> GetGuilds(); Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); - Task GetInvite(string inviteIdOrXkcd); + Task GetInvite(string inviteIdOrXkcd); Task GetUser(ulong id); Task GetUser(string username, ushort discriminator); @@ -36,6 +42,5 @@ namespace Discord Task> GetVoiceRegions(); Task GetVoiceRegion(string id); - Task GetOptimalVoiceRegion(); } } diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs index f8679d0ec..ccc7f06f7 100644 --- a/src/Discord.Net/Logging/ILogger.cs +++ b/src/Discord.Net/Logging/ILogger.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Discord.Logging { @@ -6,28 +7,28 @@ namespace Discord.Logging { LogSeverity Level { get; } - void Log(LogSeverity severity, string message, Exception exception = null); - void Log(LogSeverity severity, FormattableString message, Exception exception = null); - void Log(LogSeverity severity, Exception exception); + Task Log(LogSeverity severity, string message, Exception exception = null); + Task Log(LogSeverity severity, FormattableString message, Exception exception = null); + Task Log(LogSeverity severity, Exception exception); - void Error(string message, Exception exception = null); - void Error(FormattableString message, Exception exception = null); - void Error(Exception exception); + Task Error(string message, Exception exception = null); + Task Error(FormattableString message, Exception exception = null); + Task Error(Exception exception); - void Warning(string message, Exception exception = null); - void Warning(FormattableString message, Exception exception = null); - void Warning(Exception exception); + Task Warning(string message, Exception exception = null); + Task Warning(FormattableString message, Exception exception = null); + Task Warning(Exception exception); - void Info(string message, Exception exception = null); - void Info(FormattableString message, Exception exception = null); - void Info(Exception exception); + Task Info(string message, Exception exception = null); + Task Info(FormattableString message, Exception exception = null); + Task Info(Exception exception); - void Verbose(string message, Exception exception = null); - void Verbose(FormattableString message, Exception exception = null); - void Verbose(Exception exception); + Task Verbose(string message, Exception exception = null); + Task Verbose(FormattableString message, Exception exception = null); + Task Verbose(Exception exception); - void Debug(string message, Exception exception = null); - void Debug(FormattableString message, Exception exception = null); - void Debug(Exception exception); + Task Debug(string message, Exception exception = null); + Task Debug(FormattableString message, Exception exception = null); + Task Debug(Exception exception); } } diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 0c183071d..5e5d819b7 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Discord.Logging { @@ -6,107 +7,107 @@ namespace Discord.Logging { public LogSeverity Level { get; } - public event EventHandler Message = delegate { }; + public event Func Message; internal LogManager(LogSeverity minSeverity) { Level = minSeverity; } - public void Log(LogSeverity severity, string source, string message, Exception ex = null) + public async Task Log(LogSeverity severity, string source, string message, Exception ex = null) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, message, ex)); + await Message.Raise(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } - public void Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) + public async Task Log(LogSeverity severity, string source, FormattableString message, Exception ex = null) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, message.ToString(), ex)); + await Message.Raise(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); } - public void Log(LogSeverity severity, string source, Exception ex) + public async Task Log(LogSeverity severity, string source, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, source, null, ex)); + await Message.Raise(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, string message, Exception ex) + async Task ILogger.Log(LogSeverity severity, string message, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", message, ex)); + await Message.Raise(new LogMessage(severity, "Discord", message, ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) + async Task ILogger.Log(LogSeverity severity, FormattableString message, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", message.ToString(), ex)); + await Message.Raise(new LogMessage(severity, "Discord", message.ToString(), ex)).ConfigureAwait(false); } - void ILogger.Log(LogSeverity severity, Exception ex) + async Task ILogger.Log(LogSeverity severity, Exception ex) { if (severity <= Level) - Message(this, new LogMessageEventArgs(severity, "Discord", null, ex)); + await Message.Raise(new LogMessage(severity, "Discord", null, ex)).ConfigureAwait(false); } - public void Error(string source, string message, Exception ex = null) + public Task Error(string source, string message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); - public void Error(string source, FormattableString message, Exception ex = null) + public Task Error(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); - public void Error(string source, Exception ex) + public Task Error(string source, Exception ex) => Log(LogSeverity.Error, source, ex); - void ILogger.Error(string message, Exception ex) + Task ILogger.Error(string message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); - void ILogger.Error(FormattableString message, Exception ex) + Task ILogger.Error(FormattableString message, Exception ex) => Log(LogSeverity.Error, "Discord", message, ex); - void ILogger.Error(Exception ex) + Task ILogger.Error(Exception ex) => Log(LogSeverity.Error, "Discord", ex); - public void Warning(string source, string message, Exception ex = null) + public Task Warning(string source, string message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); - public void Warning(string source, FormattableString message, Exception ex = null) + public Task Warning(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); - public void Warning(string source, Exception ex) + public Task Warning(string source, Exception ex) => Log(LogSeverity.Warning, source, ex); - void ILogger.Warning(string message, Exception ex) + Task ILogger.Warning(string message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); - void ILogger.Warning(FormattableString message, Exception ex) + Task ILogger.Warning(FormattableString message, Exception ex) => Log(LogSeverity.Warning, "Discord", message, ex); - void ILogger.Warning(Exception ex) + Task ILogger.Warning(Exception ex) => Log(LogSeverity.Warning, "Discord", ex); - public void Info(string source, string message, Exception ex = null) + public Task Info(string source, string message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); - public void Info(string source, FormattableString message, Exception ex = null) + public Task Info(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); - public void Info(string source, Exception ex) + public Task Info(string source, Exception ex) => Log(LogSeverity.Info, source, ex); - void ILogger.Info(string message, Exception ex) + Task ILogger.Info(string message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); - void ILogger.Info(FormattableString message, Exception ex) + Task ILogger.Info(FormattableString message, Exception ex) => Log(LogSeverity.Info, "Discord", message, ex); - void ILogger.Info(Exception ex) + Task ILogger.Info(Exception ex) => Log(LogSeverity.Info, "Discord", ex); - public void Verbose(string source, string message, Exception ex = null) + public Task Verbose(string source, string message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); - public void Verbose(string source, FormattableString message, Exception ex = null) + public Task Verbose(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); - public void Verbose(string source, Exception ex) + public Task Verbose(string source, Exception ex) => Log(LogSeverity.Verbose, source, ex); - void ILogger.Verbose(string message, Exception ex) + Task ILogger.Verbose(string message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); - void ILogger.Verbose(FormattableString message, Exception ex) + Task ILogger.Verbose(FormattableString message, Exception ex) => Log(LogSeverity.Verbose, "Discord", message, ex); - void ILogger.Verbose(Exception ex) + Task ILogger.Verbose(Exception ex) => Log(LogSeverity.Verbose, "Discord", ex); - public void Debug(string source, string message, Exception ex = null) + public Task Debug(string source, string message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, FormattableString message, Exception ex = null) + public Task Debug(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, Exception ex) + public Task Debug(string source, Exception ex) => Log(LogSeverity.Debug, source, ex); - void ILogger.Debug(string message, Exception ex) + Task ILogger.Debug(string message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); - void ILogger.Debug(FormattableString message, Exception ex) + Task ILogger.Debug(FormattableString message, Exception ex) => Log(LogSeverity.Debug, "Discord", message, ex); - void ILogger.Debug(Exception ex) + Task ILogger.Debug(Exception ex) => Log(LogSeverity.Debug, "Discord", ex); internal Logger CreateLogger(string name) => new Logger(this, name); diff --git a/src/Discord.Net/Common/Events/LogMessageEventArgs.cs b/src/Discord.Net/Logging/LogMessage.cs similarity index 91% rename from src/Discord.Net/Common/Events/LogMessageEventArgs.cs rename to src/Discord.Net/Logging/LogMessage.cs index dd8c885ee..14bc4e263 100644 --- a/src/Discord.Net/Common/Events/LogMessageEventArgs.cs +++ b/src/Discord.Net/Logging/LogMessage.cs @@ -3,14 +3,14 @@ using System.Text; namespace Discord { - public class LogMessageEventArgs : EventArgs + public struct LogMessage { public LogSeverity Severity { get; } public string Source { get; } public string Message { get; } public Exception Exception { get; } - public LogMessageEventArgs(LogSeverity severity, string source, string message, Exception exception = null) + public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) { Severity = severity; Source = source; diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 274655948..74435e012 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Discord.Logging { @@ -15,44 +16,44 @@ namespace Discord.Logging Name = name; } - public void Log(LogSeverity severity, string message, Exception exception = null) + public Task Log(LogSeverity severity, string message, Exception exception = null) => _manager.Log(severity, Name, message, exception); - public void Log(LogSeverity severity, FormattableString message, Exception exception = null) + public Task Log(LogSeverity severity, FormattableString message, Exception exception = null) => _manager.Log(severity, Name, message, exception); - public void Error(string message, Exception exception = null) + public Task Error(string message, Exception exception = null) => _manager.Error(Name, message, exception); - public void Error(FormattableString message, Exception exception = null) + public Task Error(FormattableString message, Exception exception = null) => _manager.Error(Name, message, exception); - public void Error(Exception exception) + public Task Error(Exception exception) => _manager.Error(Name, exception); - public void Warning(string message, Exception exception = null) + public Task Warning(string message, Exception exception = null) => _manager.Warning(Name, message, exception); - public void Warning(FormattableString message, Exception exception = null) + public Task Warning(FormattableString message, Exception exception = null) => _manager.Warning(Name, message, exception); - public void Warning(Exception exception) + public Task Warning(Exception exception) => _manager.Warning(Name, exception); - public void Info(string message, Exception exception = null) + public Task Info(string message, Exception exception = null) => _manager.Info(Name, message, exception); - public void Info(FormattableString message, Exception exception = null) + public Task Info(FormattableString message, Exception exception = null) => _manager.Info(Name, message, exception); - public void Info(Exception exception) + public Task Info(Exception exception) => _manager.Info(Name, exception); - public void Verbose(string message, Exception exception = null) + public Task Verbose(string message, Exception exception = null) => _manager.Verbose(Name, message, exception); - public void Verbose(FormattableString message, Exception exception = null) + public Task Verbose(FormattableString message, Exception exception = null) => _manager.Verbose(Name, message, exception); - public void Verbose(Exception exception) + public Task Verbose(Exception exception) => _manager.Verbose(Name, exception); - public void Debug(string message, Exception exception = null) + public Task Debug(string message, Exception exception = null) => _manager.Debug(Name, message, exception); - public void Debug(FormattableString message, Exception exception = null) + public Task Debug(FormattableString message, Exception exception = null) => _manager.Debug(Name, message, exception); - public void Debug(Exception exception) + public Task Debug(Exception exception) => _manager.Debug(Name, exception); } } diff --git a/src/Discord.Net/LoginState.cs b/src/Discord.Net/LoginState.cs new file mode 100644 index 000000000..42b6ecac9 --- /dev/null +++ b/src/Discord.Net/LoginState.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum LoginState : byte + { + LoggedOut, + LoggingIn, + LoggedIn, + LoggingOut + } +} diff --git a/src/Discord.Net/Common/Helpers/MentionHelper.cs b/src/Discord.Net/MentionUtils.cs similarity index 94% rename from src/Discord.Net/Common/Helpers/MentionHelper.cs rename to src/Discord.Net/MentionUtils.cs index d2e350b2b..7d37ffc58 100644 --- a/src/Discord.Net/Common/Helpers/MentionHelper.cs +++ b/src/Discord.Net/MentionUtils.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; namespace Discord { - public static class MentionHelper + public static class MentionUtils { private static readonly Regex _userRegex = new Regex(@"<@!?([0-9]+)>", RegexOptions.Compiled); private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); @@ -64,11 +64,11 @@ namespace Discord } /// Gets the ids of all users mentioned in a provided text. - public static IReadOnlyList GetUserMentions(string text) => GetMentions(text, _userRegex).ToArray(); + public static IImmutableList GetUserMentions(string text) => GetMentions(text, _userRegex).ToImmutableArray(); /// Gets the ids of all channels mentioned in a provided text. - public static IReadOnlyList GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToArray(); + public static IImmutableList GetChannelMentions(string text) => GetMentions(text, _channelRegex).ToImmutableArray(); /// Gets the ids of all roles mentioned in a provided text. - public static IReadOnlyList GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToArray(); + public static IImmutableList GetRoleMentions(string text) => GetMentions(text, _roleRegex).ToImmutableArray(); private static ImmutableArray.Builder GetMentions(string text, Regex regex) { var matches = regex.Matches(text); diff --git a/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs b/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs index 95c4479df..48bcbd755 100644 --- a/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs +++ b/src/Discord.Net/Net/Converters/ChannelTypeConverter.cs @@ -5,7 +5,9 @@ namespace Discord.Net.Converters { public class ChannelTypeConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(ChannelType); + public static readonly ChannelTypeConverter Instance = new ChannelTypeConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs new file mode 100644 index 000000000..678dc83cd --- /dev/null +++ b/src/Discord.Net/Net/Converters/DiscordContractResolver.cs @@ -0,0 +1,77 @@ +using Discord.API; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Discord.Net.Converters +{ + public class DiscordContractResolver : DefaultContractResolver + { + private static readonly TypeInfo _ienumerable = typeof(IEnumerable).GetTypeInfo(); + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + var propInfo = member as PropertyInfo; + + if (propInfo != null) + { + JsonConverter converter = null; + var type = property.PropertyType; + var typeInfo = type.GetTypeInfo(); + + //Primitives + if (propInfo.GetCustomAttribute() == null) + { + if (type == typeof(ulong)) + converter = UInt64Converter.Instance; + else if (type == typeof(ulong?)) + converter = NullableUInt64Converter.Instance; + else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEnumerable))) + converter = UInt64ArrayConverter.Instance; + } + if (converter == null) + { + //Enums + if (type == typeof(ChannelType)) + converter = ChannelTypeConverter.Instance; + else if (type == typeof(PermissionTarget)) + converter = PermissionTargetConverter.Instance; + else if (type == typeof(UserStatus)) + converter = UserStatusConverter.Instance; + + //Entities + if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + converter = UInt64EntityConverter.Instance; + else if (typeInfo.ImplementedInterfaces.Any(x => x == typeof(IEntity))) + converter = StringEntityConverter.Instance; + + //Special + else if (type == typeof(string) && propInfo.GetCustomAttribute() != null) + 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); + converter = OptionalConverter.Instance; + } + } + + if (converter != null) + { + property.Converter = converter; + property.MemberConverter = converter; + } + } + + return property; + } + } +} diff --git a/src/Discord.Net/Net/Converters/ImageConverter.cs b/src/Discord.Net/Net/Converters/ImageConverter.cs index 5fc25e8d2..a40b5bf86 100644 --- a/src/Discord.Net/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net/Net/Converters/ImageConverter.cs @@ -7,7 +7,9 @@ namespace Discord.Net.Converters { public class ImageConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(Stream) || objectType == typeof(Optional); + public static readonly ImageConverter Instance = new ImageConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs b/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs index e28460abc..050ac7c32 100644 --- a/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs +++ b/src/Discord.Net/Net/Converters/NullableUInt64Converter.cs @@ -6,7 +6,9 @@ namespace Discord.Net.Converters { public class NullableUInt64Converter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(ulong?); + public static readonly NullableUInt64Converter Instance = new NullableUInt64Converter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/OptionalContractResolver.cs b/src/Discord.Net/Net/Converters/OptionalContractResolver.cs deleted file mode 100644 index cc0705671..000000000 --- a/src/Discord.Net/Net/Converters/OptionalContractResolver.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Discord.API; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System; -using System.Linq.Expressions; -using System.Reflection; - -namespace Discord.Net.Converters -{ - public class OptionalContractResolver : DefaultContractResolver - { - private static readonly PropertyInfo _isSpecified = typeof(IOptional).GetProperty(nameof(IOptional.IsSpecified)); - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var property = base.CreateProperty(member, memberSerialization); - var type = property.PropertyType; - - if (member.MemberType == MemberTypes.Property) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) - { - var parentArg = Expression.Parameter(typeof(object)); - var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); - var isSpecified = Expression.Property(optional, _isSpecified); - var lambda = Expression.Lambda>(isSpecified, parentArg).Compile(); - property.ShouldSerialize = x => lambda(x); - } - } - - return property; - } - } -} diff --git a/src/Discord.Net/Net/Converters/OptionalConverter.cs b/src/Discord.Net/Net/Converters/OptionalConverter.cs index e75769e5f..aa1abe9e2 100644 --- a/src/Discord.Net/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net/Net/Converters/OptionalConverter.cs @@ -1,12 +1,16 @@ using Discord.API; using Newtonsoft.Json; using System; +using System.Reflection; namespace Discord.Net.Converters { public class OptionalConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>); + 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; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs index a7ef55409..6dc74932f 100644 --- a/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs +++ b/src/Discord.Net/Net/Converters/PermissionTargetConverter.cs @@ -5,7 +5,9 @@ namespace Discord.Net.Converters { public class PermissionTargetConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(PermissionTarget); + public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/StringEntityConverter.cs b/src/Discord.Net/Net/Converters/StringEntityConverter.cs index 49c52bd9d..902fb1a75 100644 --- a/src/Discord.Net/Net/Converters/StringEntityConverter.cs +++ b/src/Discord.Net/Net/Converters/StringEntityConverter.cs @@ -5,7 +5,9 @@ namespace Discord.Net.Converters { public class StringEntityConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public static readonly StringEntityConverter Instance = new StringEntityConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => false; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs b/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs index f57e3427b..d0a8d170b 100644 --- a/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs +++ b/src/Discord.Net/Net/Converters/UInt64ArrayConverter.cs @@ -5,9 +5,11 @@ using System.Globalization; namespace Discord.Net.Converters { - internal class UInt64ArrayConverter : JsonConverter + public class UInt64ArrayConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); + public static readonly UInt64ArrayConverter Instance = new UInt64ArrayConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/UInt64Converter.cs b/src/Discord.Net/Net/Converters/UInt64Converter.cs index 4983759ab..6cbcd81f6 100644 --- a/src/Discord.Net/Net/Converters/UInt64Converter.cs +++ b/src/Discord.Net/Net/Converters/UInt64Converter.cs @@ -6,7 +6,9 @@ namespace Discord.Net.Converters { public class UInt64Converter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(ulong); + public static readonly UInt64Converter Instance = new UInt64Converter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs b/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs index 6a0705e3a..8a102ab22 100644 --- a/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs +++ b/src/Discord.Net/Net/Converters/UInt64EntityConverter.cs @@ -1,11 +1,14 @@ using Newtonsoft.Json; using System; +using System.Globalization; namespace Discord.Net.Converters { public class UInt64EntityConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public static readonly UInt64EntityConverter Instance = new UInt64EntityConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => false; public override bool CanWrite => true; @@ -17,7 +20,7 @@ namespace Discord.Net.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value != null) - writer.WriteValue((value as IEntity).Id); + writer.WriteValue((value as IEntity).Id.ToString(CultureInfo.InvariantCulture)); else writer.WriteNull(); } diff --git a/src/Discord.Net/Net/Converters/UserStatusConverter.cs b/src/Discord.Net/Net/Converters/UserStatusConverter.cs index 7ed690421..d2c25d3b8 100644 --- a/src/Discord.Net/Net/Converters/UserStatusConverter.cs +++ b/src/Discord.Net/Net/Converters/UserStatusConverter.cs @@ -5,7 +5,9 @@ namespace Discord.Net.Converters { public class UserStatusConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(UserStatus); + public static readonly UserStatusConverter Instance = new UserStatusConverter(); + + public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => true; diff --git a/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs b/src/Discord.Net/Net/Queue/BucketGroup.cs similarity index 71% rename from src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs rename to src/Discord.Net/Net/Queue/BucketGroup.cs index 54c3e717d..161f08432 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/BucketGroup.cs +++ b/src/Discord.Net/Net/Queue/BucketGroup.cs @@ -1,4 +1,4 @@ -namespace Discord.Net.Rest +namespace Discord.Net.Queue { internal enum BucketGroup { diff --git a/src/Discord.Net/Net/Queue/GlobalBucket.cs b/src/Discord.Net/Net/Queue/GlobalBucket.cs new file mode 100644 index 000000000..d1e011ffd --- /dev/null +++ b/src/Discord.Net/Net/Queue/GlobalBucket.cs @@ -0,0 +1,12 @@ +namespace Discord.Net.Queue +{ + public enum GlobalBucket + { + General, + Login, + DirectMessage, + SendEditMessage, + Gateway, + UpdateStatus + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs b/src/Discord.Net/Net/Queue/GuildBucket.cs similarity index 71% rename from src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs rename to src/Discord.Net/Net/Queue/GuildBucket.cs index ccb3fa994..4089fd1e7 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/GuildBucket.cs +++ b/src/Discord.Net/Net/Queue/GuildBucket.cs @@ -1,10 +1,11 @@ -namespace Discord.Net.Rest +namespace Discord.Net.Queue { public enum GuildBucket { SendEditMessage, DeleteMessage, DeleteMessages, + ModifyMember, Nickname } } diff --git a/src/Discord.Net/Net/Queue/IQueuedRequest.cs b/src/Discord.Net/Net/Queue/IQueuedRequest.cs new file mode 100644 index 000000000..e5575046e --- /dev/null +++ b/src/Discord.Net/Net/Queue/IQueuedRequest.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal interface IQueuedRequest + { + TaskCompletionSource Promise { get; } + CancellationToken CancelToken { get; } + Task Send(); + } +} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs b/src/Discord.Net/Net/Queue/IRequestQueue.cs similarity index 87% rename from src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs rename to src/Discord.Net/Net/Queue/IRequestQueue.cs index 67adbf924..75a820934 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/IRequestQueue.cs +++ b/src/Discord.Net/Net/Queue/IRequestQueue.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { //TODO: Add docstrings public interface IRequestQueue diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs b/src/Discord.Net/Net/Queue/RequestQueue.cs similarity index 62% rename from src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs rename to src/Discord.Net/Net/Queue/RequestQueue.cs index 155b683e7..365ebfb68 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueue.cs +++ b/src/Discord.Net/Net/Queue/RequestQueue.cs @@ -4,26 +4,49 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { public class RequestQueue : IRequestQueue { - private SemaphoreSlim _lock; - private RequestQueueBucket[] _globalBuckets; - private Dictionary[] _guildBuckets; - - public IRestClient RestClient { get; } - - public RequestQueue(IRestClient restClient) + private readonly SemaphoreSlim _lock; + private readonly RequestQueueBucket[] _globalBuckets; + private readonly Dictionary[] _guildBuckets; + private CancellationTokenSource _clearToken; + private CancellationToken _parentToken; + private CancellationToken _cancelToken; + + public RequestQueue() { - RestClient = restClient; - _lock = new SemaphoreSlim(1, 1); _globalBuckets = new RequestQueueBucket[Enum.GetValues(typeof(GlobalBucket)).Length]; _guildBuckets = new Dictionary[Enum.GetValues(typeof(GuildBucket)).Length]; + + _clearToken = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + } + public async Task SetCancelToken(CancellationToken cancelToken) + { + await Lock().ConfigureAwait(false); + try + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; + } + finally { Unlock(); } } - - internal async Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + + internal Task Send(RestRequest request, BucketGroup group, int bucketId, ulong guildId) + { + request.CancelToken = _cancelToken; + return Send(request as IQueuedRequest, group, bucketId, guildId); + } + internal Task Send(WebSocketRequest request, BucketGroup group, int bucketId, ulong guildId) + { + request.CancelToken = _cancelToken; + return Send(request as IQueuedRequest, group, bucketId, guildId); + } + private async Task Send(IQueuedRequest request, BucketGroup group, int bucketId, ulong guildId) { RequestQueueBucket bucket; @@ -47,7 +70,11 @@ namespace Discord.Net.Rest { //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)); } @@ -57,9 +84,10 @@ namespace Discord.Net.Rest switch (bucket) { //Per Guild - case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5); + case GuildBucket.SendEditMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 5, GlobalBucket.SendEditMessage); case GuildBucket.DeleteMessage: return new RequestQueueBucket(this, bucket, guildId, 5, 1); case GuildBucket.DeleteMessages: return new RequestQueueBucket(this, bucket, guildId, 1, 1); + case GuildBucket.ModifyMember: return new RequestQueueBucket(this, bucket, guildId, 10, 10); //TODO: Is this all users or just roles? case GuildBucket.Nickname: return new RequestQueueBucket(this, bucket, guildId, 1, 1); default: throw new ArgumentException($"Unknown guild bucket: {bucket}", nameof(bucket)); @@ -105,13 +133,13 @@ namespace Discord.Net.Rest return bucket; } - internal void DestroyGlobalBucket(GlobalBucket type) + public void DestroyGlobalBucket(GlobalBucket type) { //Assume this object is locked _globalBuckets[(int)type] = null; } - internal void DestroyGuildBucket(GuildBucket type, ulong guildId) + public void DestroyGuildBucket(GuildBucket type, ulong guildId) { //Assume this object is locked @@ -129,6 +157,20 @@ namespace Discord.Net.Rest _lock.Release(); } + public async Task Clear() + { + await Lock().ConfigureAwait(false); + try + { + _clearToken?.Cancel(); + _clearToken = new CancellationTokenSource(); + if (_parentToken != null) + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; + else + _cancelToken = _clearToken.Token; + } + finally { Unlock(); } + } public async Task Clear(GlobalBucket type) { var bucket = _globalBuckets[(int)type]; @@ -136,7 +178,7 @@ namespace Discord.Net.Rest { try { - await bucket.Lock(); + await bucket.Lock().ConfigureAwait(false); bucket.Clear(); } finally { bucket.Unlock(); } @@ -152,7 +194,7 @@ namespace Discord.Net.Rest { try { - await bucket.Lock(); + await bucket.Lock().ConfigureAwait(false); bucket.Clear(); } finally { bucket.Unlock(); } diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs similarity index 70% rename from src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs rename to src/Discord.Net/Net/Queue/RequestQueueBucket.cs index 2d14bc367..7b05fb0fe 100644 --- a/src/Discord.Net/Net/Rest/RequestQueue/RequestQueueBucket.cs +++ b/src/Discord.Net/Net/Queue/RequestQueueBucket.cs @@ -5,55 +5,54 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net.Rest +namespace Discord.Net.Queue { + //TODO: Implement bucket chaining internal class RequestQueueBucket { private readonly RequestQueue _parent; private readonly BucketGroup _bucketGroup; + private readonly GlobalBucket? _chainedBucket; private readonly int _bucketId; private readonly ulong _guildId; - private readonly ConcurrentQueue _queue; + private readonly ConcurrentQueue _queue; private readonly SemaphoreSlim _lock; private Task _resetTask; - private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed + private bool _waitingToProcess; private int _id; public int WindowMaxCount { get; } public int WindowSeconds { get; } public int WindowCount { get; private set; } - public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds) - : this(parent, windowMaxCount, windowSeconds) + public RequestQueueBucket(RequestQueue parent, GlobalBucket bucket, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) + : this(parent, windowMaxCount, windowSeconds, chainedBucket) { _bucketGroup = BucketGroup.Global; _bucketId = (int)bucket; _guildId = 0; } - public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds) - : this(parent, windowMaxCount, windowSeconds) + public RequestQueueBucket(RequestQueue parent, GuildBucket bucket, ulong guildId, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) + : this(parent, windowMaxCount, windowSeconds, chainedBucket) { _bucketGroup = BucketGroup.Guild; _bucketId = (int)bucket; _guildId = guildId; } - private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds) + private RequestQueueBucket(RequestQueue parent, int windowMaxCount, int windowSeconds, GlobalBucket? chainedBucket = null) { _parent = parent; WindowMaxCount = windowMaxCount; WindowSeconds = windowSeconds; - _queue = new ConcurrentQueue(); + _chainedBucket = chainedBucket; + _queue = new ConcurrentQueue(); _lock = new SemaphoreSlim(1, 1); _id = new System.Random().Next(0, int.MaxValue); } - public void Queue(RestRequest request) + public void Queue(IQueuedRequest request) { - if (_destroyed) throw new Exception(); - //Assume this obj's parent is under lock - _queue.Enqueue(request); - Debug($"Request queued ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); } public async Task ProcessQueue(bool acquireLock = false) { @@ -72,7 +71,7 @@ namespace Discord.Net.Rest _waitingToProcess = false; while (true) { - RestRequest request; + IQueuedRequest request; //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort if (WindowCount == WindowMaxCount) return; @@ -81,12 +80,13 @@ namespace Discord.Net.Rest try { - Stream stream; - if (request.IsMultipart) - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.MultipartParams, request.HeaderOnly).ConfigureAwait(false); + if (request.CancelToken.IsCancellationRequested) + request.Promise.SetException(new OperationCanceledException(request.CancelToken)); else - stream = await _parent.RestClient.Send(request.Method, request.Endpoint, request.Json, request.HeaderOnly).ConfigureAwait(false); - request.Promise.SetResult(stream); + { + 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 { @@ -94,17 +94,13 @@ namespace Discord.Net.Rest var task = _resetTask; if (task != null) { - Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); await task.ConfigureAwait(false); int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); _resetTask = ResetAfter(millis); } else - { - Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); _resetTask = ResetAfter(ex.RetryAfterMilliseconds); - } return; } catch (HttpException ex) @@ -128,13 +124,11 @@ namespace Discord.Net.Rest _queue.TryDequeue(out request); WindowCount++; nextRetry = 1000; - Debug($"Request succeeded ({WindowCount}/{WindowMaxCount} + {_queue.Count})"); if (WindowCount == 1 && WindowSeconds > 0) { //First request for this window, schedule a reset _resetTask = ResetAfter(WindowSeconds * 1000); - Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms"); } } @@ -145,11 +139,7 @@ namespace Discord.Net.Rest { await _parent.Lock().ConfigureAwait(false); if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks - { - Debug($"Destroy"); _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); - _destroyed = true; - } } finally { @@ -166,7 +156,7 @@ namespace Discord.Net.Rest public void Clear() { //Assume this obj is under lock - RestRequest request; + IQueuedRequest request; while (_queue.TryDequeue(out request)) { } } @@ -179,8 +169,6 @@ namespace Discord.Net.Rest { await Lock().ConfigureAwait(false); - Debug($"Reset"); - //Reset the current window count and set our state back to normal WindowCount = 0; _resetTask = null; @@ -188,10 +176,7 @@ namespace Discord.Net.Rest //Wait is over, work through the current queue await ProcessQueue().ConfigureAwait(false); } - finally - { - Unlock(); - } + finally { Unlock(); } } public async Task Lock() @@ -202,24 +187,5 @@ namespace Discord.Net.Rest { _lock.Release(); } - - //TODO: Remove - private void Debug(string text) - { - string name; - switch (_bucketGroup) - { - case BucketGroup.Global: - name = ((GlobalBucket)_bucketId).ToString(); - break; - case BucketGroup.Guild: - name = ((GuildBucket)_bucketId).ToString(); - break; - default: - name = "Unknown"; - break; - } - System.Diagnostics.Debug.WriteLine($"[{name} {_id}] {text}"); - } } } diff --git a/src/Discord.Net/Net/Queue/RestRequest.cs b/src/Discord.Net/Net/Queue/RestRequest.cs new file mode 100644 index 000000000..7c71d114a --- /dev/null +++ b/src/Discord.Net/Net/Queue/RestRequest.cs @@ -0,0 +1,55 @@ +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class RestRequest : IQueuedRequest + { + public IRestClient Client { get; } + public string Method { get; } + public string Endpoint { get; } + public string Json { get; } + public bool HeaderOnly { get; } + public IReadOnlyDictionary MultipartParams { get; } + public TaskCompletionSource Promise { get; } + public CancellationToken CancelToken { get; set; } + + public bool IsMultipart => MultipartParams != null; + + public RestRequest(IRestClient client, string method, string endpoint, string json, bool headerOnly) + : this(client, method, endpoint, headerOnly) + { + Json = json; + } + + public RestRequest(IRestClient client, string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) + : this(client, method, endpoint, headerOnly) + { + MultipartParams = multipartParams; + } + + private RestRequest(IRestClient client, string method, string endpoint, bool headerOnly) + { + Client = client; + Method = method; + Endpoint = endpoint; + Json = null; + MultipartParams = null; + HeaderOnly = headerOnly; + Promise = new TaskCompletionSource(); + } + + public async Task Send() + { + if (IsMultipart) + return await Client.Send(Method, Endpoint, MultipartParams, HeaderOnly).ConfigureAwait(false); + else if (Json != null) + return await Client.Send(Method, Endpoint, Json, HeaderOnly).ConfigureAwait(false); + else + return await Client.Send(Method, Endpoint, HeaderOnly).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Net/Queue/WebSocketRequest.cs b/src/Discord.Net/Net/Queue/WebSocketRequest.cs new file mode 100644 index 000000000..bd8f492c3 --- /dev/null +++ b/src/Discord.Net/Net/Queue/WebSocketRequest.cs @@ -0,0 +1,35 @@ +using Discord.Net.WebSockets; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Queue +{ + internal class WebSocketRequest : IQueuedRequest + { + public IWebSocketClient Client { get; } + public byte[] Data { get; } + public int DataIndex { get; } + public int DataCount { get; } + public bool IsText { 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) + { + Client = client; + Data = data; + DataIndex = index; + DataCount = count; + IsText = isText; + Promise = new TaskCompletionSource(); + } + + public async Task Send() + { + await Client.Send(Data, DataIndex, DataCount, IsText).ConfigureAwait(false); + return null; + } + } +} diff --git a/src/Discord.Net/Net/Rest/DefaultRestClient.cs b/src/Discord.Net/Net/Rest/DefaultRestClient.cs index a2b859197..9d83e9a32 100644 --- a/src/Discord.Net/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net/Net/Rest/DefaultRestClient.cs @@ -17,13 +17,13 @@ namespace Discord.Net.Rest protected readonly HttpClient _client; protected readonly string _baseUrl; - protected readonly CancellationToken _cancelToken; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; protected bool _isDisposed; - public DefaultRestClient(string baseUrl, CancellationToken cancelToken) + public DefaultRestClient(string baseUrl) { _baseUrl = baseUrl; - _cancelToken = cancelToken; _client = new HttpClient(new HttpClientHandler { @@ -32,8 +32,11 @@ namespace Discord.Net.Rest UseProxy = false, PreAuthenticate = false }); - SetHeader("accept-encoding", "gzip, deflate"); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; } protected virtual void Dispose(bool disposing) { @@ -55,18 +58,27 @@ namespace Discord.Net.Rest if (value != null) _client.DefaultRequestHeaders.Add(key, value); } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } - public async Task Send(string method, string endpoint, string json = null, bool headerOnly = false) + public async Task Send(string method, string endpoint, bool headerOnly = false) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); + } + public async Task Send(string method, string endpoint, string json, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { - if (json != null) - restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false); + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); } } - public async Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false) { string uri = Path.Combine(_baseUrl, endpoint); @@ -77,6 +89,7 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { +#if CSHARP7 switch (p.Value) { case string value: @@ -94,30 +107,35 @@ namespace Discord.Net.Rest default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } +#else + var stringValue = p.Value as string; + if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } + var byteArrayValue = p.Value as byte[]; + if (byteArrayValue != null) { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + var streamValue = p.Value as Stream; + if (streamValue != null) { content.Add(new StreamContent(streamValue), p.Key); continue; } + if (p.Value is MultipartFile) + { + var fileValue = (MultipartFile)p.Value; + content.Add(new StreamContent(fileValue.Stream), fileValue.Filename, p.Key); + continue; + } + + throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); +#endif } } restRequest.Content = content; - return await SendInternal(restRequest, _cancelToken, headerOnly).ConfigureAwait(false); + return await SendInternal(restRequest, headerOnly).ConfigureAwait(false); } } - private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + private async Task SendInternal(HttpRequestMessage request, bool headerOnly) { - int retryCount = 0; while (true) { - HttpResponseMessage response; - try - { - response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - } - catch (WebException ex) - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw; - } + var cancelToken = _cancelToken; //It's okay if another thread changes this, causes a retry to abort + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); int statusCode = (int)response.StatusCode; if (statusCode < 200 || statusCode >= 300) //2xx = Success diff --git a/src/Discord.Net/Net/Rest/IRestClient.cs b/src/Discord.Net/Net/Rest/IRestClient.cs index 3f99a2f7e..25b577688 100644 --- a/src/Discord.Net/Net/Rest/IRestClient.cs +++ b/src/Discord.Net/Net/Rest/IRestClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Rest @@ -8,8 +9,10 @@ namespace Discord.Net.Rest public interface IRestClient { void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); - Task Send(string method, string endpoint, string json = null, bool headerOnly = false); + Task Send(string method, string endpoint, bool headerOnly = false); + Task Send(string method, string endpoint, string json, bool headerOnly = false); Task Send(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly = false); } } diff --git a/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs b/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs deleted file mode 100644 index 4e7126f5e..000000000 --- a/src/Discord.Net/Net/Rest/RequestQueue/GlobalBucket.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord.Net.Rest -{ - public enum GlobalBucket - { - General, - DirectMessage - } -} diff --git a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs b/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs deleted file mode 100644 index 098dccc8a..000000000 --- a/src/Discord.Net/Net/Rest/RequestQueue/RestRequest.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord.Net.Rest -{ - internal struct RestRequest - { - public string Method { get; } - public string Endpoint { get; } - public string Json { get; } - public bool HeaderOnly { get; } - public IReadOnlyDictionary MultipartParams { get; } - public TaskCompletionSource Promise { get; } - - public bool IsMultipart => MultipartParams != null; - - public RestRequest(string method, string endpoint, string json, bool headerOnly) - : this(method, endpoint, headerOnly) - { - Json = json; - } - - public RestRequest(string method, string endpoint, IReadOnlyDictionary multipartParams, bool headerOnly) - : this(method, endpoint, headerOnly) - { - MultipartParams = multipartParams; - } - - private RestRequest(string method, string endpoint, bool headerOnly) - { - Method = method; - Endpoint = endpoint; - Json = null; - MultipartParams = null; - HeaderOnly = headerOnly; - Promise = new TaskCompletionSource(); - } - } -} diff --git a/src/Discord.Net/Net/Rest/RestClientProvider.cs b/src/Discord.Net/Net/Rest/RestClientProvider.cs index cf3ee0846..51a7eb619 100644 --- a/src/Discord.Net/Net/Rest/RestClientProvider.cs +++ b/src/Discord.Net/Net/Rest/RestClientProvider.cs @@ -1,6 +1,4 @@ -using System.Threading; - -namespace Discord.Net.Rest +namespace Discord.Net.Rest { - public delegate IRestClient RestClientProvider(string baseUrl, CancellationToken cancelToken); + public delegate IRestClient RestClientProvider(string baseUrl); } diff --git a/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs new file mode 100644 index 000000000..d9559a2cf --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs @@ -0,0 +1,156 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + public class DefaultWebSocketClient : IWebSocketClient + { + public const int ReceiveChunkSize = 12 * 1024; //12KB + public const int SendChunkSize = 4 * 1024; //4KB + private const int HR_TIMEOUT = -2147012894; + + public event Func BinaryMessage; + public event Func TextMessage; + + private readonly ClientWebSocket _client; + private Task _task; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed; + + public DefaultWebSocketClient() + { + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public async Task Connect(string host) + { + //Assume locked + await Disconnect().ConfigureAwait(false); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + + await _client.ConnectAsync(new Uri(host), _cancelToken).ConfigureAwait(false); + _task = Run(_cancelToken); + } + public async Task Disconnect() + { + //Assume locked + _cancelTokenSource.Cancel(); + + if (_client.State == WebSocketState.Open) + try { await _client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } catch { } + + await (_task ?? Task.CompletedTask).ConfigureAwait(false); + } + + public void SetHeader(string key, string value) + { + _client.Options.SetRequestHeader(key, value); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } + + public async Task Send(byte[] data, int index, int count, bool isText) + { + //TODO: If connection is temporarily down, retry? + int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); + + for (int i = 0; i < frameCount; i++, index += SendChunkSize) + { + bool isLast = i == (frameCount - 1); + + int frameSize; + if (isLast) + frameSize = count - (i * SendChunkSize); + else + frameSize = SendChunkSize; + + try + { + await _client.SendAsync(new ArraySegment(data, index, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + return; + } + } + } + + //TODO: Check this code + private async Task Run(CancellationToken cancelToken) + { + var buffer = new ArraySegment(new byte[ReceiveChunkSize]); + var stream = new MemoryStream(); + + try + { + while (!cancelToken.IsCancellationRequested) + { + WebSocketReceiveResult result = null; + do + { + if (cancelToken.IsCancellationRequested) return; + + try + { + result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + } + catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) + { + throw new Exception("Connection timed out."); + } + + if (result.MessageType == WebSocketMessageType.Close) + throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); + else + stream.Write(buffer.Array, 0, result.Count); + + } + while (result == null || !result.EndOfMessage); + + var array = stream.ToArray(); + if (result.MessageType == WebSocketMessageType.Binary) + await BinaryMessage.Raise(array, 0, array.Length).ConfigureAwait(false); + else if (result.MessageType == WebSocketMessageType.Text) + { + string text = Encoding.UTF8.GetString(array, 0, array.Length); + await TextMessage.Raise(text).ConfigureAwait(false); + } + + stream.Position = 0; + stream.SetLength(0); + } + } + catch (OperationCanceledException) { } + } + } +} diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs new file mode 100644 index 000000000..2925c1350 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/IWebSocketClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.WebSockets +{ + public interface IWebSocketClient + { + event Func BinaryMessage; + event Func TextMessage; + + void SetHeader(string key, string value); + void SetCancelToken(CancellationToken cancelToken); + + Task Connect(string host); + Task Disconnect(); + + Task Send(byte[] data, int index, int count, bool isText); + } +} diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs new file mode 100644 index 000000000..88f467221 --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.Net.WebSockets +{ + public delegate IWebSocketClient WebSocketProvider(); +} diff --git a/src/Discord.Net/Preconditions.cs b/src/Discord.Net/Preconditions.cs new file mode 100644 index 000000000..1bd8da7ac --- /dev/null +++ b/src/Discord.Net/Preconditions.cs @@ -0,0 +1,152 @@ +using Discord.API; +using System; + +namespace Discord +{ + internal static class Preconditions + { + //Objects + public static void NotNull(T obj, string name) where T : class { if (obj == null) throw new ArgumentNullException(name); } + public static void NotNull(Optional obj, string name) where T : class { if (obj.IsSpecified && obj.Value == null) throw new ArgumentNullException(name); } + + //Strings + public static void NotEmpty(string obj, string name) { if (obj.Length == 0) throw new ArgumentException("Argument cannot be empty.", name); } + public static void NotEmpty(Optional obj, string name) { if (obj.IsSpecified && obj.Value.Length == 0) throw new ArgumentException("Argument cannot be empty.", name); } + public static void NotNullOrEmpty(string obj, string name) + { + if (obj == null) + throw new ArgumentNullException(name); + if (obj.Length == 0) + throw new ArgumentException("Argument cannot be empty.", name); + } + public static void NotNullOrEmpty(Optional obj, string name) + { + if (obj.IsSpecified) + { + if (obj.Value == null) + throw new ArgumentNullException(name); + if (obj.Value.Length == 0) + throw new ArgumentException("Argument cannot be empty.", name); + } + } + public static void NotNullOrWhitespace(string obj, string name) + { + if (obj == null) + throw new ArgumentNullException(name); + if (obj.Trim().Length == 0) + throw new ArgumentException("Argument cannot be blank.", name); + } + public static void NotNullOrWhitespace(Optional obj, string name) + { + if (obj.IsSpecified) + { + if (obj.Value == null) + throw new ArgumentNullException(name); + if (obj.Value.Trim().Length == 0) + throw new ArgumentException("Argument cannot be blank.", name); + } + } + + //Numerics + public static void NotEqual(sbyte obj, sbyte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(byte obj, byte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(short obj, short value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(ushort obj, ushort value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(int obj, int value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(uint obj, uint value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(long obj, long value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(ulong obj, ulong value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(sbyte? obj, sbyte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(byte? obj, byte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(short? obj, short value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(ushort? obj, ushort value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(int? obj, int value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(uint? obj, uint value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(long? obj, long value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(ulong? obj, ulong value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } + + public static void AtLeast(sbyte obj, sbyte value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(byte obj, byte value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(short obj, short value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(ushort obj, ushort value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(int obj, int value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(uint obj, uint value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(long obj, long value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(ulong obj, ulong value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + public static void AtLeast(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } + + public static void GreaterThan(sbyte obj, sbyte value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(byte obj, byte value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(short obj, short value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(ushort obj, ushort value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(int obj, int value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(uint obj, uint value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(long obj, long value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(ulong obj, ulong value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + public static void GreaterThan(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } + + public static void AtMost(sbyte obj, sbyte value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(byte obj, byte value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(short obj, short value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(ushort obj, ushort value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(int obj, int value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(uint obj, uint value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(long obj, long value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(ulong obj, ulong value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + public static void AtMost(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } + + public static void LessThan(sbyte obj, sbyte value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(byte obj, byte value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(short obj, short value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(ushort obj, ushort value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(int obj, int value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(uint obj, uint value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(long obj, long value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(ulong obj, ulong value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void LessThan(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + } +} diff --git a/src/Discord.Net/Properties/AssemblyInfo.cs b/src/Discord.Net/Properties/AssemblyInfo.cs index 26e8c428e..7dcbdb315 100644 --- a/src/Discord.Net/Properties/AssemblyInfo.cs +++ b/src/Discord.Net/Properties/AssemblyInfo.cs @@ -1,16 +1,19 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle("Discord.Net")] -[assembly: AssemblyProduct("Discord.Net")] -[assembly: AssemblyDescription("An unofficial .Net API wrapper for the Discord client.")] -[assembly: AssemblyCompany("RogueException")] -[assembly: AssemblyCopyright("Copyright © RogueException 2016")] +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Discord.Net.Core")] +[assembly: AssemblyTrademark("")] +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -[assembly: Guid("62ea817d-c945-4100-ba21-9dfb139d2868")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0-alpha1")] +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("91e9e7bd-75c9-4e98-84aa-2c271922e5c2")] diff --git a/src/Discord.Net/Rest/DiscordClient.cs b/src/Discord.Net/Rest/DiscordClient.cs index c719fab3e..89475ca93 100644 --- a/src/Discord.Net/Rest/DiscordClient.cs +++ b/src/Discord.Net/Rest/DiscordClient.cs @@ -1,51 +1,53 @@ using Discord.API.Rest; using Discord.Logging; using Discord.Net; +using Discord.Net.Queue; using Discord.Net.Rest; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; namespace Discord.Rest { + //TODO: Docstrings + //TODO: Log Internal/External REST Rate Limits, 502s + //TODO: Log Logins/Logouts public sealed class DiscordClient : IDiscordClient, IDisposable { - public event EventHandler Log; - public event EventHandler LoggedIn, LoggedOut; + public event Func Log; + public event Func LoggedIn, LoggedOut; + private readonly Logger _discordLogger, _restLogger; private readonly SemaphoreSlim _connectionLock; private readonly RestClientProvider _restClientProvider; private readonly LogManager _log; - private CancellationTokenSource _cancelTokenSource; + private readonly RequestQueue _requestQueue; private bool _isDisposed; - private string _userAgent; private SelfUser _currentUser; - public bool IsLoggedIn { get; private set; } - public API.DiscordRawClient BaseClient { get; private set; } - - public TokenType AuthTokenType => BaseClient.AuthTokenType; - public IRestClient RestClient => BaseClient.RestClient; - public IRequestQueue RequestQueue => BaseClient.RequestQueue; + 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(); - - _restClientProvider = config.RestClientProvider; + + _log = new LogManager(config.LogLevel); + _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); + _discordLogger = _log.CreateLogger("Discord"); + _restLogger = _log.CreateLogger("Rest"); _connectionLock = new SemaphoreSlim(1, 1); - _log = new LogManager(config.LogLevel); - _userAgent = DiscordConfig.UserAgent; - BaseClient = new API.DiscordRawClient(_restClientProvider, _cancelTokenSource.Token); + _requestQueue = new RequestQueue(); - _log.Message += (s,e) => Log.Raise(this, e); + 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) @@ -53,7 +55,7 @@ namespace Discord.Rest await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(email, password).ConfigureAwait(false); + await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false); } finally { _connectionLock.Release(); } } @@ -62,93 +64,86 @@ namespace Discord.Rest await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); + await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task LoginInternal(string email, string password) + private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) { - if (IsLoggedIn) - LogoutInternal(); + if (LoginState != LoginState.LoggedOut) + await LogoutInternal().ConfigureAwait(false); + LoginState = LoginState.LoggingIn; + try { - var cancelTokenSource = new CancellationTokenSource(); + if (useEmail) + { + var args = new LoginParams { Email = email, Password = password }; + await ApiClient.Login(args).ConfigureAwait(false); + } + else + await ApiClient.Login(tokenType, token).ConfigureAwait(false); - var args = new LoginParams { Email = email, Password = password }; - await BaseClient.Login(args).ConfigureAwait(false); - await CompleteLogin(cancelTokenSource, false).ConfigureAwait(false); + if (validateToken) + { + try + { + await ApiClient.ValidateToken().ConfigureAwait(false); + } + catch (HttpException ex) + { + throw new ArgumentException("Token validation failed", nameof(token), ex); + } + } + + LoginState = LoginState.LoggedIn; } - catch { LogoutInternal(); throw; } - } - private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) - { - if (IsLoggedIn) - LogoutInternal(); - try + catch (Exception) { - var cancelTokenSource = new CancellationTokenSource(); - - BaseClient.SetToken(tokenType, token); - await CompleteLogin(cancelTokenSource, validateToken).ConfigureAwait(false); + await LogoutInternal().ConfigureAwait(false); + throw; } - catch { LogoutInternal(); throw; } - } - private async Task CompleteLogin(CancellationTokenSource cancelTokenSource, bool validateToken) - { - BaseClient.SentRequest += (s, e) => _log.Verbose("Rest", $"{e.Method} {e.Endpoint}: {e.Milliseconds} ms"); - - if (validateToken) - await BaseClient.ValidateToken().ConfigureAwait(false); - _cancelTokenSource = cancelTokenSource; - IsLoggedIn = true; - LoggedIn.Raise(this); + await LoggedIn.Raise().ConfigureAwait(false); } public async Task Logout() { - _cancelTokenSource?.Cancel(); await _connectionLock.WaitAsync().ConfigureAwait(false); try { - LogoutInternal(); + await LogoutInternal().ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private void LogoutInternal() + private async Task LogoutInternal() { - bool wasLoggedIn = IsLoggedIn; + if (LoginState == LoginState.LoggedOut) return; + LoginState = LoginState.LoggingOut; - if (_cancelTokenSource != null) - { - try { _cancelTokenSource.Cancel(false); } - catch { } - } + await ApiClient.Logout().ConfigureAwait(false); - BaseClient.SetToken(TokenType.User, null); _currentUser = null; - if (wasLoggedIn) - { - IsLoggedIn = false; - LoggedOut.Raise(this); - } + LoginState = LoginState.LoggedOut; + + await LoggedOut.Raise().ConfigureAwait(false); } public async Task> GetConnections() { - var models = await BaseClient.GetCurrentUserConnections().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); return models.Select(x => new Connection(x)); } public async Task GetChannel(ulong id) { - var model = await BaseClient.GetChannel(id).ConfigureAwait(false); + var model = await ApiClient.GetChannel(id).ConfigureAwait(false); if (model != null) { if (model.GuildId != null) { - var guildModel = await BaseClient.GetGuild(model.GuildId.Value).ConfigureAwait(false); + var guildModel = await ApiClient.GetGuild(model.GuildId.Value).ConfigureAwait(false); if (guildModel != null) { var guild = new Guild(this, guildModel); @@ -162,55 +157,55 @@ namespace Discord.Rest } public async Task> GetDMChannels() { - var models = await BaseClient.GetCurrentUserDMs().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); return models.Select(x => new DMChannel(this, x)); } - public async Task GetInvite(string inviteIdOrXkcd) + public async Task GetInvite(string inviteIdOrXkcd) { - var model = await BaseClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); + var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); if (model != null) - return new PublicInvite(this, model); + return new Invite(this, model); return null; } public async Task GetGuild(ulong id) { - var model = await BaseClient.GetGuild(id).ConfigureAwait(false); + var model = await ApiClient.GetGuild(id).ConfigureAwait(false); if (model != null) return new Guild(this, model); return null; } public async Task GetGuildEmbed(ulong id) { - var model = await BaseClient.GetGuildEmbed(id).ConfigureAwait(false); + var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); if (model != null) return new GuildEmbed(model); return null; } public async Task> GetGuilds() { - var models = await BaseClient.GetCurrentUserGuilds().ConfigureAwait(false); + var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); return models.Select(x => new UserGuild(this, x)); } public async Task CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) { var args = new CreateGuildParams(); - var model = await BaseClient.CreateGuild(args).ConfigureAwait(false); + var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); return new Guild(this, model); } public async Task GetUser(ulong id) { - var model = await BaseClient.GetUser(id).ConfigureAwait(false); + var model = await ApiClient.GetUser(id).ConfigureAwait(false); if (model != null) return new PublicUser(this, model); return null; } public async Task GetUser(string username, ushort discriminator) { - var model = await BaseClient.GetUser(username, discriminator).ConfigureAwait(false); + var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); if (model != null) return new PublicUser(this, model); return null; @@ -220,7 +215,7 @@ namespace Discord.Rest var user = _currentUser; if (user == null) { - var model = await BaseClient.GetCurrentUser().ConfigureAwait(false); + var model = await ApiClient.GetCurrentUser().ConfigureAwait(false); user = new SelfUser(this, model); _currentUser = user; } @@ -228,46 +223,40 @@ namespace Discord.Rest } public async Task> QueryUsers(string query, int limit) { - var models = await BaseClient.QueryUsers(query, limit).ConfigureAwait(false); + var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); return models.Select(x => new PublicUser(this, x)); } public async Task> GetVoiceRegions() { - var models = await BaseClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)); } public async Task GetVoiceRegion(string id) { - var models = await BaseClient.GetVoiceRegions().ConfigureAwait(false); + var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); } - public async Task GetOptimalVoiceRegion() - { - var models = await BaseClient.GetVoiceRegions().ConfigureAwait(false); - return models.Select(x => new VoiceRegion(x)).Where(x => x.IsOptimal).FirstOrDefault(); - } void Dispose(bool disposing) { if (!_isDisposed) - { - if (disposing) - _cancelTokenSource.Dispose(); _isDisposed = true; - } } public void Dispose() => Dispose(true); - API.DiscordRawClient IDiscordClient.BaseClient => BaseClient; + 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) + async Task IDiscordClient.GetInvite(string inviteIdOrXkcd) => await GetInvite(inviteIdOrXkcd).ConfigureAwait(false); async Task IDiscordClient.GetGuild(ulong id) => await GetGuild(id).ConfigureAwait(false); @@ -287,7 +276,5 @@ namespace Discord.Rest => await GetVoiceRegions().ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegion(string id) => await GetVoiceRegion(id).ConfigureAwait(false); - async Task IDiscordClient.GetOptimalVoiceRegion() - => await GetOptimalVoiceRegion().ConfigureAwait(false); } } diff --git a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs index 3a76aa702..0efa29da3 100644 --- a/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/DMChannel.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class DMChannel : IDMChannel { /// @@ -16,10 +18,10 @@ namespace Discord.Rest internal DiscordClient Discord { get; } /// - public DMUser Recipient { get; private set; } + public User Recipient { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); internal DMChannel(DiscordClient discord, Model model) { @@ -31,13 +33,13 @@ namespace Discord.Rest private void Update(Model model) { if (Recipient == null) - Recipient = new DMUser(this, model.Recipient); + Recipient = new PublicUser(Discord, model.Recipient); else Recipient.Update(model.Recipient); } /// - public async Task GetUser(ulong id) + public async Task GetUser(ulong id) { var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); if (id == Recipient.Id) @@ -48,24 +50,24 @@ namespace Discord.Rest return null; } /// - public async Task> GetUsers() + public async Task> GetUsers() { var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); - return ImmutableArray.Create(currentUser, Recipient); + return ImmutableArray.Create(currentUser, Recipient); } /// public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false); + 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.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } @@ -73,7 +75,7 @@ namespace Discord.Rest public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.CreateMessage(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); return new Message(this, model); } /// @@ -83,7 +85,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.UploadFile(Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); return new Message(this, model); } } @@ -91,46 +93,50 @@ namespace Discord.Rest 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.BaseClient.UploadFile(Id, stream, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); return new Message(this, model); } /// public async Task DeleteMessages(IEnumerable messages) { - await Discord.BaseClient.DeleteMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } /// public async Task Close() { - await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// public async Task Update() { - var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); Update(model); } /// - public override string ToString() => $"@{Recipient} [DM]"; - - IDMUser IDMChannel.Recipient => Recipient; + 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.GetMessage(ulong id) - => throw new NotSupportedException(); + 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) diff --git a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs index c14e918fe..66e0abe19 100644 --- a/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/GuildChannel.cs @@ -23,7 +23,7 @@ namespace Discord.Rest public int Position { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); /// public IReadOnlyDictionary PermissionOverwrites => _overwrites; internal DiscordClient Discord => Guild.Discord; @@ -55,21 +55,11 @@ namespace Discord.Rest var args = new ModifyGuildChannelParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + 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 model = await Discord.BaseClient.GetGuildMember(Guild.Id, id).ConfigureAwait(false); - if (model != null) - return new GuildUser(Guild, model); - return null; - } - protected abstract Task> GetUsers(); - - /// Gets the permission overwrite for a specific user, or null if one does not exist. + + /// public OverwritePermissions? GetPermissionOverwrite(IUser user) { Overwrite value; @@ -77,7 +67,7 @@ namespace Discord.Rest return value.Permissions; return null; } - /// Gets the permission overwrite for a specific role, or null if one does not exist. + /// public OverwritePermissions? GetPermissionOverwrite(IRole role) { Overwrite value; @@ -86,38 +76,38 @@ namespace Discord.Rest return null; } /// Downloads a collection of all invites to this channel. - public async Task> GetInvites() + public async Task> GetInvites() { - var models = await Discord.BaseClient.GetChannelInvites(Id).ConfigureAwait(false); - return models.Select(x => new GuildInvite(Guild, x)); + var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); + return models.Select(x => new InviteMetadata(Discord, x)); } - /// Adds or updates the permission overwrite for the given user. + /// public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.BaseClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); + 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 }); } - /// Adds or updates the permission overwrite for the given role. + /// public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; - await Discord.BaseClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); + 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 }); } - /// Removes the permission overwrite for the given user, if one exists. + /// public async Task RemovePermissionOverwrite(IUser user) { - await Discord.BaseClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(user.Id, out value); } - /// Removes the permission overwrite for the given role, if one exists. + /// public async Task RemovePermissionOverwrite(IRole role) { - await Discord.BaseClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); Overwrite value; _overwrites.TryRemove(role.Id, out value); @@ -128,7 +118,7 @@ namespace Discord.Rest /// 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) + public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) { var args = new CreateChannelInviteParams { @@ -137,34 +127,43 @@ namespace Discord.Rest Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.BaseClient.CreateChannelInvite(Id, args).ConfigureAwait(false); - return new GuildInvite(Guild, model); + var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); + return new InviteMetadata(Discord, model); } /// public async Task Delete() { - await Discord.BaseClient.DeleteChannel(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); } /// public async Task Update() { - var model = await Discord.BaseClient.GetChannel(Id).ConfigureAwait(false); + 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); + IGuild IGuildChannel.Guild => Guild; - async Task IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) + async Task IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); - async Task> IGuildChannel.GetInvites() + async Task> IGuildChannel.GetInvites() => await GetInvites().ConfigureAwait(false); async Task> IGuildChannel.GetUsers() - => await GetUsers().ConfigureAwait(false); - async Task IGuildChannel.GetUser(ulong id) - => await GetUser(id).ConfigureAwait(false); + => await GetUsersInternal().ConfigureAwait(false); async Task> IChannel.GetUsers() - => await GetUsers().ConfigureAwait(false); + => 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 GetUser(id).ConfigureAwait(false); + => await GetUserInternal(id).ConfigureAwait(false); } } diff --git a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs index e15d7578a..4c171bea2 100644 --- a/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/TextChannel.cs @@ -1,6 +1,7 @@ using Discord.API.Rest; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -8,13 +9,14 @@ 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 => MentionHelper.Mention(this); + public string Mention => MentionUtils.Mention(this); internal TextChannel(Guild guild, Model model) : base(guild, model) @@ -33,30 +35,43 @@ namespace Discord.Rest var args = new ModifyTextChannelParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); Update(model); } - protected override async Task> GetUsers() + /// 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 => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.ReadMessages)); + 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 Task GetMessage(ulong id) { throw new NotSupportedException(); } //Not implemented /// public async Task> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) { var args = new GetChannelMessagesParams { Limit = limit }; - var models = await Discord.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false); + 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.BaseClient.GetChannelMessages(Id, args).ConfigureAwait(false); + var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); return models.Select(x => new Message(this, x)); } @@ -64,7 +79,7 @@ namespace Discord.Rest public async Task SendMessage(string text, bool isTTS = false) { var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); return new Message(this, model); } /// @@ -74,7 +89,7 @@ namespace Discord.Rest using (var file = File.OpenRead(filePath)) { var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; - var model = await Discord.BaseClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); + var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); return new Message(this, model); } } @@ -82,27 +97,33 @@ namespace Discord.Rest 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.BaseClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); + 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.BaseClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); + await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParam { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); } /// public async Task TriggerTyping() { - await Discord.BaseClient.TriggerTypingIndicator(Id).ConfigureAwait(false); + await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); } - /// - public override string ToString() => $"{base.ToString()} [Text]"; + 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(); - async Task IMessageChannel.GetMessage(ulong id) - => await GetMessage(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) diff --git a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs index 703b58c01..e105aabd6 100644 --- a/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs +++ b/src/Discord.Net/Rest/Entities/Channels/VoiceChannel.cs @@ -1,16 +1,19 @@ using Discord.API.Rest; using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Channel; namespace Discord.Rest { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class VoiceChannel : GuildChannel, IVoiceChannel { /// public int Bitrate { get; private set; } + /// + public int UserLimit { get; private set; } internal VoiceChannel(Guild guild, Model model) : base(guild, model) @@ -20,6 +23,7 @@ namespace Discord.Rest { base.Update(model); Bitrate = model.Bitrate; + UserLimit = model.UserLimit; } /// @@ -29,17 +33,14 @@ namespace Discord.Rest var args = new ModifyVoiceChannelParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); Update(model); } - protected override async Task> GetUsers() - { - var users = await Guild.GetUsers().ConfigureAwait(false); - return users.Where(x => PermissionUtilities.GetValue(PermissionHelper.Resolve(x, this), ChannelPermission.Connect)); - } + 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 string ToString() => $"{base.ToString()} [Voice]"; + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs index 5763193bc..936a0d35c 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/Guild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/Guild.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using Model = Discord.API.Guild; using EmbedModel = Discord.API.GuildEmbed; using RoleModel = Discord.API.Role; +using System.Diagnostics; namespace Discord.Rest { /// Represents a Discord guild (called a server in the official client). + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Guild : IGuild { private ConcurrentDictionary _roles; @@ -44,7 +46,7 @@ namespace Discord.Rest public IReadOnlyList Features { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); /// public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); /// @@ -113,7 +115,7 @@ namespace Discord.Rest /// public async Task Update() { - var response = await Discord.BaseClient.GetGuild(Id).ConfigureAwait(false); + var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); Update(response); } /// @@ -123,7 +125,7 @@ namespace Discord.Rest var args = new ModifyGuildParams(); func(args); - var model = await Discord.BaseClient.ModifyGuild(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); Update(model); } /// @@ -133,40 +135,35 @@ namespace Discord.Rest var args = new ModifyGuildEmbedParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); - + var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); Update(model); } /// public async Task ModifyChannels(IEnumerable args) { - if (args == null) throw new NullReferenceException(nameof(args)); - - await Discord.BaseClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); } /// public async Task ModifyRoles(IEnumerable args) - { - if (args == null) throw new NullReferenceException(nameof(args)); - - var models = await Discord.BaseClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); + { + var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); Update(models); } /// public async Task Leave() { - await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } /// public async Task> GetBans() { - var models = await Discord.BaseClient.GetGuildBans(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); return models.Select(x => new PublicUser(Discord, x)); } /// @@ -174,24 +171,21 @@ namespace Discord.Rest /// public async Task AddBan(ulong userId, int pruneDays = 0) { - var args = new CreateGuildBanParams() - { - PruneDays = pruneDays - }; - await Discord.BaseClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); + 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.BaseClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); + 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) { - var model = await Discord.BaseClient.GetChannel(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); if (model != null) return ToChannel(model); return null; @@ -199,7 +193,7 @@ namespace Discord.Rest /// Gets a collection of all channels in this guild. public async Task> GetChannels() { - var models = await Discord.BaseClient.GetGuildChannels(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); return models.Select(x => ToChannel(x)); } /// Creates a new text channel. @@ -208,7 +202,7 @@ namespace Discord.Rest if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams() { Name = name, Type = ChannelType.Text }; - var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); return new TextChannel(this, model); } /// Creates a new voice channel. @@ -217,32 +211,32 @@ namespace Discord.Rest if (name == null) throw new ArgumentNullException(nameof(name)); var args = new CreateGuildChannelParams { Name = name, Type = ChannelType.Voice }; - var model = await Discord.BaseClient.CreateGuildChannel(Id, args).ConfigureAwait(false); + 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() { - var models = await Discord.BaseClient.GetGuildIntegrations(Id).ConfigureAwait(false); + var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); return models.Select(x => new GuildIntegration(this, x)); } /// 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.BaseClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); + 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.BaseClient.GetGuildInvites(Id).ConfigureAwait(false); - return models.Select(x => new GuildInvite(this, x)); + 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) + 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)); @@ -254,8 +248,8 @@ namespace Discord.Rest Temporary = isTemporary, XkcdPass = withXkcd }; - var model = await Discord.BaseClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); - return new GuildInvite(this, model); + 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. @@ -272,7 +266,7 @@ namespace Discord.Rest { if (name == null) throw new ArgumentNullException(nameof(name)); - var model = await Discord.BaseClient.CreateGuildRole(Id).ConfigureAwait(false); + var model = await Discord.ApiClient.CreateGuildRole(Id).ConfigureAwait(false); var role = new Role(this, model); await role.Modify(x => @@ -290,20 +284,20 @@ namespace Discord.Rest public async Task> GetUsers() { var args = new GetGuildMembersParams(); - var models = await Discord.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false); + 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.BaseClient.GetGuildMembers(Id, args).ConfigureAwait(false); + 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) { - var model = await Discord.BaseClient.GetGuildMember(Id, id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); if (model != null) return new GuildUser(this, model); return null; @@ -319,9 +313,9 @@ namespace Discord.Rest var args = new GuildPruneParams() { Days = days }; GetGuildPruneCountResponse model; if (simulate) - model = await Discord.BaseClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.GetGuildPruneCount(Id, args).ConfigureAwait(false); else - model = await Discord.BaseClient.BeginGuildPrune(Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.BeginGuildPrune(Id, args).ConfigureAwait(false); return model.Pruned; } @@ -337,7 +331,8 @@ namespace Discord.Rest } } - public override string ToString() => Name ?? Id.ToString(); + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; IEnumerable IGuild.Emojis => Emojis; ulong IGuild.EveryoneRoleId => EveryoneRole.Id; @@ -349,7 +344,7 @@ namespace Discord.Rest => 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) + 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); @@ -357,7 +352,7 @@ namespace Discord.Rest => await CreateTextChannel(name).ConfigureAwait(false); async Task IGuild.CreateVoiceChannel(string name) => await CreateVoiceChannel(name).ConfigureAwait(false); - async Task> IGuild.GetInvites() + async Task> IGuild.GetInvites() => await GetInvites().ConfigureAwait(false); Task IGuild.GetRole(ulong id) => Task.FromResult(GetRole(id)); diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs index 5d9220ade..d7f5a3831 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/GuildEmbed.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using Model = Discord.API.GuildEmbed; -namespace Discord.Rest +namespace Discord { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class GuildEmbed : IGuildEmbed { /// @@ -12,14 +14,11 @@ namespace Discord.Rest /// public ulong? ChannelId { get; private set; } - internal DiscordClient Discord { get; } - /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); - internal GuildEmbed(DiscordClient discord, Model model) + internal GuildEmbed(Model model) { - Discord = discord; Update(model); } @@ -29,6 +28,7 @@ namespace Discord.Rest IsEnabled = model.Enabled; } - public override string ToString() => $"{Id} ({(IsEnabled ? "Enabled" : "Disabled")})"; + public override string ToString() => Id.ToString(); + private string DebuggerDisplay => $"{Id}{(IsEnabled ? " (Enabled)" : "")}"; } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs index c479f9f4d..e368cc8d7 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/GuildIntegration.cs @@ -1,10 +1,12 @@ using Discord.API.Rest; using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Integration; namespace Discord.Rest { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class GuildIntegration : IGuildIntegration { /// @@ -58,7 +60,7 @@ namespace Discord.Rest /// public async Task Delete() { - await Discord.BaseClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } /// public async Task Modify(Action func) @@ -67,21 +69,22 @@ namespace Discord.Rest var args = new ModifyGuildIntegrationParams(); func(args); - var model = await Discord.BaseClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); Update(model); } /// public async Task Sync() { - await Discord.BaseClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); } - public override string ToString() => $"{Name ?? Id.ToString()} ({(IsEnabled ? "Enabled" : "Disabled")})"; + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; IGuild IGuildIntegration.Guild => Guild; IRole IGuildIntegration.Role => Role; IUser IGuildIntegration.User => User; - IIntegrationAccount IGuildIntegration.Account => Account; + IntegrationAccount IGuildIntegration.Account => Account; } } diff --git a/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs deleted file mode 100644 index f28061955..000000000 --- a/src/Discord.Net/Rest/Entities/Guilds/IntegrationAccount.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Discord.Rest -{ - public class IntegrationAccount : IIntegrationAccount - { - /// - public string Id { get; } - - /// - public string Name { get; private set; } - - public override string ToString() => Name ?? Id.ToString(); - } -} diff --git a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs index cae71f5ae..ae5c31da3 100644 --- a/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs +++ b/src/Discord.Net/Rest/Entities/Guilds/UserGuild.cs @@ -1,16 +1,18 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.UserGuild; -namespace Discord.Rest +namespace Discord { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class UserGuild : IUserGuild { private string _iconId; /// public ulong Id { get; } - internal DiscordClient Discord { get; } + internal IDiscordClient Discord { get; } /// public string Name { get; private set; } @@ -18,11 +20,11 @@ namespace Discord.Rest public GuildPermissions Permissions { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); /// public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); - internal UserGuild(DiscordClient discord, Model model) + internal UserGuild(IDiscordClient discord, Model model) { Discord = discord; Id = model.Id; @@ -40,18 +42,15 @@ namespace Discord.Rest /// public async Task Leave() { - if (IsOwner) - throw new InvalidOperationException("Unable to leave a guild the current user owns."); - await Discord.BaseClient.LeaveGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); } /// public async Task Delete() { - if (!IsOwner) - throw new InvalidOperationException("Unable to delete a guild the current user does not own."); - await Discord.BaseClient.DeleteGuild(Id).ConfigureAwait(false); + await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); } - public override string ToString() => Name ?? Id.ToString(); + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsOwner ? ", Owned" : "")})"; } } diff --git a/src/Discord.Net/Rest/Entities/Invites/GuildInvite.cs b/src/Discord.Net/Rest/Entities/Invites/GuildInvite.cs deleted file mode 100644 index 98087d694..000000000 --- a/src/Discord.Net/Rest/Entities/Invites/GuildInvite.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Threading.Tasks; -using Model = Discord.API.InviteMetadata; - -namespace Discord.Rest -{ - public class GuildInvite : Invite, IGuildInvite - { - /// Gets the guild this invite is linked to. - public Guild Guild { get; private set; } - /// - public ulong ChannelId { get; private set; } - - /// - 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 override DiscordClient Discord => Guild.Discord; - - internal GuildInvite(Guild guild, Model model) - : base(model) - { - Guild = guild; - - Update(model); //Causes base.Update(Model) to be run twice, but that's fine. - } - private void Update(Model model) - { - base.Update(model); - IsRevoked = model.Revoked; - IsTemporary = model.Temporary; - MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; - MaxUses = model.MaxUses; - Uses = model.Uses; - } - - /// - public async Task Delete() - { - await Discord.BaseClient.DeleteInvite(Code).ConfigureAwait(false); - } - - IGuild IGuildInvite.Guild => Guild; - ulong IInvite.GuildId => Guild.Id; - } -} diff --git a/src/Discord.Net/Rest/Entities/Invites/Invite.cs b/src/Discord.Net/Rest/Entities/Invites/Invite.cs deleted file mode 100644 index 2e13b0542..000000000 --- a/src/Discord.Net/Rest/Entities/Invites/Invite.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using Model = Discord.API.Invite; - -namespace Discord.Rest -{ - public abstract class Invite : IInvite - { - protected ulong _guildId, _channelId; - - /// - public string Code { get; } - /// - public string XkcdCode { get; } - - /// - public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; - /// - public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; - - internal abstract DiscordClient Discord { get; } - - internal Invite(Model model) - { - Code = model.Code; - XkcdCode = model.XkcdPass; - - Update(model); - } - protected virtual void Update(Model model) - { - _guildId = model.Guild.Id; - _channelId = model.Channel.Id; - } - - /// - public async Task Accept() - { - await Discord.BaseClient.AcceptInvite(Code).ConfigureAwait(false); - } - - /// - public override string ToString() => XkcdUrl ?? Url; - - string IEntity.Id => Code; - ulong IInvite.GuildId => _guildId; - ulong IInvite.ChannelId => _channelId; - } -} diff --git a/src/Discord.Net/Rest/Entities/Invites/PublicInvite.cs b/src/Discord.Net/Rest/Entities/Invites/PublicInvite.cs deleted file mode 100644 index 8a767dc20..000000000 --- a/src/Discord.Net/Rest/Entities/Invites/PublicInvite.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Model = Discord.API.Invite; - -namespace Discord.Rest -{ - public class PublicInvite : Invite, IPublicInvite - { - /// - public string GuildName { get; private set; } - /// - public string ChannelName { get; private set; } - - /// - public ulong GuildId => _guildId; - /// - public ulong ChannelId => _channelId; - - internal override DiscordClient Discord { get; } - - internal PublicInvite(DiscordClient discord, Model model) - : base(model) - { - Discord = discord; - } - protected override void Update(Model model) - { - base.Update(model); - GuildName = model.Guild.Name; - ChannelName = model.Channel.Name; - } - - /// - public async Task Update() - { - var model = await Discord.BaseClient.GetInvite(Code).ConfigureAwait(false); - Update(model); - } - } -} diff --git a/src/Discord.Net/Rest/Entities/Message.cs b/src/Discord.Net/Rest/Entities/Message.cs index 24c3eb4df..319394214 100644 --- a/src/Discord.Net/Rest/Entities/Message.cs +++ b/src/Discord.Net/Rest/Entities/Message.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Message; namespace Discord.Rest { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Message : IMessage { /// @@ -26,21 +28,21 @@ namespace Discord.Rest /// public IMessageChannel Channel { get; } /// - public User Author { 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 MentionedUsers { get; private set; } /// public IReadOnlyList MentionedChannelIds { get; private set; } /// public IReadOnlyList MentionedRoleIds { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; internal Message(IMessageChannel channel, Model model) @@ -53,6 +55,10 @@ namespace Discord.Rest } private void Update(Model model) { + var guildChannel = Channel as GuildChannel; + var guild = guildChannel?.Guild; + var discord = Discord; + IsTTS = model.IsTextToSpeech; Timestamp = model.Timestamp; EditedTimestamp = model.EditedTimestamp; @@ -78,38 +84,32 @@ namespace Discord.Rest else Embeds = Array.Empty(); - if (model.Mentions.Length > 0) + if (guildChannel != null && model.Mentions.Length > 0) { - var discord = Discord; - var builder = ImmutableArray.CreateBuilder(model.Mentions.Length); + var mentions = new PublicUser[model.Mentions.Length]; for (int i = 0; i < model.Mentions.Length; i++) - builder.Add(new PublicUser(discord, model.Mentions[i])); - MentionedUsers = builder.ToArray(); + mentions[i] = new PublicUser(discord, model.Mentions[i]); + MentionedUsers = ImmutableArray.Create(mentions); } else MentionedUsers = Array.Empty(); - MentionedChannelIds = MentionHelper.GetChannelMentions(model.Content); - MentionedRoleIds = MentionHelper.GetRoleMentions(model.Content); - if (model.IsMentioningEveryone) + + if (guildChannel != null) { - ulong? guildId = (Channel as IGuildChannel)?.Guild.Id; - if (guildId != null) - { - if (MentionedRoleIds.Count == 0) - MentionedRoleIds = ImmutableArray.Create(guildId.Value); - else - { - var builder = ImmutableArray.CreateBuilder(MentionedRoleIds.Count + 1); - builder.AddRange(MentionedRoleIds); - builder.Add(guildId.Value); - MentionedRoleIds = builder.ToImmutable(); - } - } + MentionedChannelIds = MentionUtils.GetChannelMentions(model.Content); + + var mentionedRoleIds = MentionUtils.GetRoleMentions(model.Content); + if (model.IsMentioningEveryone) + mentionedRoleIds = mentionedRoleIds.Add(guildChannel.Guild.EveryoneRole.Id); + MentionedRoleIds = mentionedRoleIds; + } + else + { + MentionedChannelIds = Array.Empty(); + MentionedRoleIds = Array.Empty(); } - Text = MentionHelper.CleanUserMentions(model.Content, model.Mentions); - - Author.Update(model.Author); + Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); } /// @@ -123,25 +123,26 @@ namespace Discord.Rest Model model; if (guildChannel != null) - model = await Discord.BaseClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); else - model = await Discord.BaseClient.ModifyMessage(Channel.Id, Id, args).ConfigureAwait(false); + model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); Update(model); } /// public async Task Delete() { - await Discord.BaseClient.DeleteMessage(Channel.Id, Id).ConfigureAwait(false); + 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() => $"{Author.ToString()}: {Text}"; + public override string ToString() => Text; + private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; IUser IMessage.Author => Author; - IReadOnlyList IMessage.Attachments => Attachments; - IReadOnlyList IMessage.Embeds => Embeds; - IReadOnlyList IMessage.MentionedChannelIds => MentionedChannelIds; IReadOnlyList IMessage.MentionedUsers => MentionedUsers; } } diff --git a/src/Discord.Net/Rest/Entities/Role.cs b/src/Discord.Net/Rest/Entities/Role.cs index 7e77f455c..20ed0940e 100644 --- a/src/Discord.Net/Rest/Entities/Role.cs +++ b/src/Discord.Net/Rest/Entities/Role.cs @@ -1,12 +1,14 @@ 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.Rest { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Role : IRole, IMentionable { /// @@ -28,11 +30,11 @@ namespace Discord.Rest public int Position { get; private set; } /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); /// public bool IsEveryone => Id == Guild.Id; /// - public string Mention => MentionHelper.Mention(this); + public string Mention => MentionUtils.Mention(this); internal DiscordClient Discord => Guild.Discord; internal Role(Guild guild, Model model) @@ -58,22 +60,23 @@ namespace Discord.Rest var args = new ModifyGuildRoleParams(); func(args); - var response = await Discord.BaseClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); + var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); Update(response); } /// Deletes this message. public async Task Delete() - => await Discord.BaseClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); + => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); /// - public override string ToString() => Name ?? Id.ToString(); - + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id})"; + ulong IRole.GuildId => Guild.Id; async Task> IRole.GetUsers() { - //A tad hacky, but it works - var models = await Discord.BaseClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); + //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/Rest/Entities/Users/Connection.cs b/src/Discord.Net/Rest/Entities/Users/Connection.cs deleted file mode 100644 index 9795dc207..000000000 --- a/src/Discord.Net/Rest/Entities/Users/Connection.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using Model = Discord.API.Connection; - -namespace Discord.Rest -{ - public class Connection : IConnection - { - public string Id { get; } - - public string Type { get; private set; } - public string Name { get; private set; } - public bool IsRevoked { get; private set; } - - public IEnumerable Integrations { get; private set; } - - public Connection(Model model) - { - Id = model.Id; - - Type = model.Type; - Name = model.Name; - IsRevoked = model.Revoked; - - Integrations = model.Integrations; - } - - public override string ToString() => $"{Name ?? Id.ToString()} ({Type})"; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/DMUser.cs b/src/Discord.Net/Rest/Entities/Users/DMUser.cs deleted file mode 100644 index 67bc534f3..000000000 --- a/src/Discord.Net/Rest/Entities/Users/DMUser.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Model = Discord.API.User; - -namespace Discord.Rest -{ - public class DMUser : User, IDMUser - { - /// - public DMChannel Channel { get; } - - internal override DiscordClient Discord => Channel.Discord; - - internal DMUser(DMChannel channel, Model model) - : base(model) - { - Channel = channel; - } - - IDMChannel IDMUser.Channel => Channel; - } -} diff --git a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs index c27c06892..33d100255 100644 --- a/src/Discord.Net/Rest/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/GuildUser.cs @@ -23,6 +23,9 @@ namespace Discord.Rest /// public string Nickname { get; private set; } + /// + public GuildPermissions GuildPermissions { get; private set; } + /// public IReadOnlyList Roles => _roles; internal override DiscordClient Discord => Guild.Discord; @@ -31,6 +34,8 @@ namespace Discord.Rest : base(model.User) { Guild = guild; + + Update(model); } internal void Update(Model model) { @@ -44,37 +49,25 @@ namespace Discord.Rest for (int i = 0; i < model.Roles.Length; i++) roles.Add(Guild.GetRole(model.Roles[i])); _roles = roles.ToImmutable(); + + GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); } public async Task Update() { - var model = await Discord.BaseClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); + var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); Update(model); } - public bool HasRole(IRole role) - { - for (int i = 0; i < _roles.Length; i++) - { - if (_roles[i].Id == role.Id) - return true; - } - return false; - } - public async Task Kick() { - await Discord.BaseClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); + await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); } - public GuildPermissions GetGuildPermissions() - { - return new GuildPermissions(PermissionHelper.Resolve(this)); - } public ChannelPermissions GetPermissions(IGuildChannel channel) { if (channel == null) throw new ArgumentNullException(nameof(channel)); - return new ChannelPermissions(PermissionHelper.Resolve(this, channel)); + return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); } public async Task Modify(Action func) @@ -87,20 +80,20 @@ namespace Discord.Rest bool isCurrentUser = (await Discord.GetCurrentUser().ConfigureAwait(false)).Id == Id; if (isCurrentUser && args.Nickname.IsSpecified) { - var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value }; - await Discord.BaseClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); + 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.BaseClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); + await Discord.ApiClient.ModifyGuildMember(Guild.Id, Id, args).ConfigureAwait(false); if (args.Deaf.IsSpecified) - IsDeaf = args.Deaf; + IsDeaf = args.Deaf.Value; if (args.Mute.IsSpecified) - IsMute = args.Mute; + IsMute = args.Mute.Value; if (args.Nickname.IsSpecified) - Nickname = args.Nickname; + Nickname = args.Nickname.Value ?? ""; if (args.Roles.IsSpecified) _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); } @@ -109,8 +102,10 @@ namespace Discord.Rest IGuild IGuildUser.Guild => Guild; IReadOnlyList IGuildUser.Roles => Roles; - ulong? IGuildUser.VoiceChannelId => null; + IVoiceChannel IGuildUser.VoiceChannel => null; + GuildPermissions IGuildUser.GetGuildPermissions() + => GuildPermissions; ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => GetPermissions(channel); } diff --git a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs index 513acf0d2..a821b369b 100644 --- a/src/Discord.Net/Rest/Entities/Users/SelfUser.cs +++ b/src/Discord.Net/Rest/Entities/Users/SelfUser.cs @@ -30,7 +30,7 @@ namespace Discord.Rest /// public async Task Update() { - var model = await Discord.BaseClient.GetCurrentUser().ConfigureAwait(false); + var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); Update(model); } @@ -41,7 +41,7 @@ namespace Discord.Rest var args = new ModifyCurrentUserParams(); func(args); - var model = await Discord.BaseClient.ModifyCurrentUser(args).ConfigureAwait(false); + var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); Update(model); } } diff --git a/src/Discord.Net/Rest/Entities/Users/User.cs b/src/Discord.Net/Rest/Entities/Users/User.cs index 9572c6620..26754fc18 100644 --- a/src/Discord.Net/Rest/Entities/Users/User.cs +++ b/src/Discord.Net/Rest/Entities/Users/User.cs @@ -1,10 +1,12 @@ using Discord.API.Rest; using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord.Rest { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public abstract class User : IUser { private string _avatarId; @@ -23,11 +25,11 @@ namespace Discord.Rest /// public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); /// - public DateTime CreatedAt => DateTimeHelper.FromSnowflake(Id); + public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); /// - public string Mention => MentionHelper.Mention(this, false); + public string Mention => MentionUtils.Mention(this, false); /// - public string NicknameMention => MentionHelper.Mention(this, true); + public string NicknameMention => MentionUtils.Mention(this, true); internal User(Model model) { @@ -43,26 +45,24 @@ namespace Discord.Rest Username = model.Username; } - public async Task CreateDMChannel() + protected virtual async Task CreateDMChannelInternal() { - var args = new CreateDMChannelParams - { - RecipientId = Id - }; - var model = await Discord.BaseClient.CreateDMChannel(args).ConfigureAwait(false); + var args = new CreateDMChannelParams { RecipientId = Id }; + var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); return new DMChannel(Discord, model); } - public override string ToString() => $"{Username ?? Id.ToString()}"; + public override string ToString() => $"{Username}#{Discriminator}"; + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; /// - string IUser.CurrentGame => null; + Game? IUser.CurrentGame => null; /// UserStatus IUser.Status => UserStatus.Unknown; /// async Task IUser.CreateDMChannel() - => await CreateDMChannel().ConfigureAwait(false); + => await CreateDMChannelInternal().ConfigureAwait(false); } } diff --git a/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs b/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs new file mode 100644 index 000000000..0b1c78317 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/DataStoreProvider.cs @@ -0,0 +1,4 @@ +namespace Discord.WebSocket.Data +{ + public delegate IDataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount); +} diff --git a/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs b/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs new file mode 100644 index 000000000..28a4ca0d1 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/DefaultDataStore.cs @@ -0,0 +1,107 @@ +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 new file mode 100644 index 000000000..b980d13d5 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/IDataStore.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 000000000..8512a2679 --- /dev/null +++ b/src/Discord.Net/WebSocket/Data/SharedDataStore.cs @@ -0,0 +1,7 @@ +namespace Discord.WebSocket.Data +{ + //TODO: Implement + /*public class SharedDataStore + { + }*/ +} diff --git a/src/Discord.Net/WebSocket/DiscordClient.cs b/src/Discord.Net/WebSocket/DiscordClient.cs new file mode 100644 index 000000000..911420731 --- /dev/null +++ b/src/Discord.Net/WebSocket/DiscordClient.cs @@ -0,0 +1,889 @@ +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/DiscordSocketConfig.cs b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs new file mode 100644 index 000000000..4318bd247 --- /dev/null +++ b/src/Discord.Net/WebSocket/DiscordSocketConfig.cs @@ -0,0 +1,42 @@ +using Discord.Net.WebSockets; +using Discord.WebSocket.Data; + +namespace Discord.WebSocket +{ + public class DiscordSocketConfig : DiscordConfig + { + /// Gets or sets the id for this shard. Must be less than TotalShards. + public int ShardId { get; set; } = 0; + /// Gets or sets the total number of shards for this application. + public int TotalShards { get; set; } = 1; + + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. + public int ConnectionTimeout { get; set; } = 30000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; + + /// Gets or sets the 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. + /// + public bool UsePermissionsCache { get; set; } = true; + /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. + public bool EnablePreUpdateEvents { get; set; } = true; + /// + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. + /// Decreasing this may reduce CPU usage while increasing login time and network usage. + /// + public int LargeThreshold { get; set; } = 250; + + //Engines + + /// Gets or sets the provider used to generate datastores. + public DataStoreProvider DataStoreProvider { get; set; } = (shardId, totalShards, guildCount, dmCount) => new DefaultDataStore(guildCount, dmCount); + /// Gets or sets the provider used to generate new websocket connections. + public WebSocketProvider WebSocketProvider { get; set; } = () => new DefaultWebSocketClient(); + } +} diff --git a/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs b/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs new file mode 100644 index 000000000..b645d1c20 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/Channel.cs @@ -0,0 +1,37 @@ +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 new file mode 100644 index 000000000..bb93566af --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/DMChannel.cs @@ -0,0 +1,144 @@ +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 new file mode 100644 index 000000000..db571577a --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/GuildChannel.cs @@ -0,0 +1,161 @@ +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 new file mode 100644 index 000000000..ade45276e --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/TextChannel.cs @@ -0,0 +1,129 @@ +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 new file mode 100644 index 000000000..d1f374499 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Channels/VoiceChannel.cs @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..c0663e924 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Guilds/Guild.cs @@ -0,0 +1,344 @@ +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 new file mode 100644 index 000000000..61610a0ae --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Guilds/GuildIntegration.cs @@ -0,0 +1,88 @@ +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 new file mode 100644 index 000000000..af42298f8 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Message.cs @@ -0,0 +1,155 @@ +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 new file mode 100644 index 000000000..52bef2b1e --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Role.cs @@ -0,0 +1,79 @@ +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 new file mode 100644 index 000000000..c0caec225 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Users/GuildUser.cs @@ -0,0 +1,143 @@ +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 new file mode 100644 index 000000000..8b8a86788 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Users/SelfUser.cs @@ -0,0 +1,40 @@ +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 new file mode 100644 index 000000000..e507b4df8 --- /dev/null +++ b/src/Discord.Net/WebSocket/Entities/Users/User.cs @@ -0,0 +1,76 @@ +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/MessageCache.cs b/src/Discord.Net/WebSocket/MessageCache.cs new file mode 100644 index 000000000..5051efc3a --- /dev/null +++ b/src/Discord.Net/WebSocket/MessageCache.cs @@ -0,0 +1,108 @@ +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; + +namespace Discord.WebSocket +{ + internal class MessageCache + { + private readonly DiscordClient _discord; + private readonly IMessageChannel _channel; + private readonly ConcurrentDictionary _messages; + private readonly ConcurrentQueue _orderedMessages; + private readonly int _size; + + public IEnumerable Messages => _messages.Select(x => x.Value); + + public MessageCache(DiscordClient discord, IMessageChannel channel) + { + _discord = discord; + _channel = channel; + _size = discord.MessageCacheSize; + _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); + _orderedMessages = new ConcurrentQueue(); + } + + internal void Add(Message message) + { + if (_messages.TryAdd(message.Id, message)) + { + _orderedMessages.Enqueue(message.Id); + + ulong msgId; + Message msg; + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) + _messages.TryRemove(msgId, out msg); + } + } + + internal void Remove(ulong id) + { + Message msg; + _messages.TryRemove(id, out msg); + } + + public Message Get(ulong id) + { + Message result; + if (_messages.TryGetValue(id, out result)) + return result; + return null; + } + 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; + + IEnumerable cachedMessageIds; + if (fromMessageId == null) + cachedMessageIds = _orderedMessages; + else if (dir == Direction.Before) + cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); + else + cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); + + return cachedMessageIds + .Take(limit) + .Select(x => + { + Message msg; + if (_messages.TryGetValue(x, out msg)) + return msg; + return null; + }) + .Where(x => x != null) + .ToImmutableArray(); + } + + public async Task> Download(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + { + //TODO: Test heavily + + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) return ImmutableArray.Empty; + + var cachedMessages = GetMany(fromMessageId, dir, limit); + if (cachedMessages.Count == limit) + return cachedMessages; + else if (cachedMessages.Count > limit) + return cachedMessages.Skip(cachedMessages.Count - limit); + else + { + var args = new GetChannelMessagesParams + { + Limit = limit - cachedMessages.Count, + RelativeDirection = dir, + RelativeMessageId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id + }; + var downloadedMessages = await _discord.ApiClient.GetChannelMessages(_channel.Id, args).ConfigureAwait(false); + //TODO: Ugly channel cast + return cachedMessages.AsEnumerable().Concat(downloadedMessages.Select(x => new Message(_channel, (_channel as Channel).GetUser(x.Id), x))).ToImmutableArray(); + } + } + } +} diff --git a/src/Discord.Net/WebSocket/PermissionsCache.cs b/src/Discord.Net/WebSocket/PermissionsCache.cs new file mode 100644 index 000000000..30f3a0b6e --- /dev/null +++ b/src/Discord.Net/WebSocket/PermissionsCache.cs @@ -0,0 +1,70 @@ +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)); + } + } + } +} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index c47cb1411..a15ef430a 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,14 +1,38 @@ { + "version": "1.0.0-dev", + "description": "An unofficial .Net API wrapper for the Discord service.", + "authors": [ "RogueException" ], + + "packOptions": { + "tags": [ "discord", "discordapp" ], + "licenseUrl": "http://opensource.org/licenses/MIT", + "projectUrl": "https://github.com/RogueException/Discord.Net", + "repository": { + "type": "git", + "url": "git://github.com/RogueException/Discord.Net" + } + }, + + "buildOptions": { + "allowUnsafe": true, + "warningsAsErrors": false + }, + "dependencies": { + "NETStandard.Library": "1.5.0-rc2-24027", "Newtonsoft.Json": "8.0.3", - "System.Collections.Immutable": "1.1.37" + "System.Collections.Immutable": "1.2.0-rc2-24027", + "System.Net.Websockets.Client": "4.0.0-rc2-24027", + "System.Runtime.Serialization.Primitives": "4.1.1-rc2-24027" }, "frameworks": { - "net461": { } - }, - - "runtimes": { - "win": { } + "netstandard1.3": { + "imports": [ + "dotnet5.4", + "dnxcore50", + "portable-net45+win8" + ] + } } -} \ No newline at end of file +}