From c461201fa5ea617d3edb0ded41d2e75e9352779a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 19:39:26 -0500 Subject: [PATCH 01/31] Fix async warnings --- test/Discord.Net.Tests/Tests.ChannelPermissions.cs | 6 +++--- test/Discord.Net.Tests/Tests.GuildPermissions.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs index c5b22e277..92234e88b 100644 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public async Task TestChannelPermission() + public void TestChannelPermission() { var perm = new ChannelPermissions(); @@ -84,7 +84,7 @@ namespace Discord Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); } - public async Task TestChannelPermissionModify() + public void TestChannelPermissionModify() { // test channel permission modify @@ -314,7 +314,7 @@ namespace Discord } [Fact] - public async Task TestChannelTypeResolution() + public void TestChannelTypeResolution() { ITextChannel someChannel = null; // null channels will throw exception diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs index a79706b9c..dc51600cf 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public async Task TestGuildPermission() + public void TestGuildPermission() { // Test Guild Permission Constructors var perm = new GuildPermissions(); @@ -49,7 +49,7 @@ namespace Discord } [Fact] - public async Task TestGuildPermissionModify() + public void TestGuildPermissionModify() { var perm = new GuildPermissions(); From bbad052ec7b5dc89e43825064aaf7d0e31bdbbf3 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 20:09:07 -0500 Subject: [PATCH 02/31] Update invite link thank you @SinisterRectus :slight_smile: --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b58d4579..bd0ef20c7 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) -[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx) +[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR) An unofficial .NET API Wrapper for the Discord client (http://discordapp.com). -Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). +Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR). ## Installation ### Stable (NuGet) From 39b5d0e74cc416a0d84d741c1d5585155c1d2075 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Nov 2017 20:13:10 -0500 Subject: [PATCH 03/31] Bumped version to 2.0.0-beta --- Discord.Net.targets | 4 ++-- appveyor.yml | 2 +- src/Discord.Net/Discord.Net.nuspec | 38 +++++++++++++++--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 95eccd790..3f623c619 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ - 2.0.0-alpha - + 2.0.0 + beta RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/appveyor.yml b/appveyor.yml index d94e2ad68..3bf70c09c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 309532615..f904f4126 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.0.0-alpha$suffix$ + 2.0.0-beta$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -13,28 +13,28 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 678a7238e6172ae539a5746fb5bc1b2c734258a0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 7 Dec 2017 16:47:01 -0500 Subject: [PATCH 04/31] Allow users to opt-in to proxies (#888) * Allow users to opt-in to proxies * Allow opting in to proxies on the WebSocket --- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 4 ++-- .../Net/DefaultRestClientProvider.cs | 23 +++++++++++------- .../Net/DefaultWebSocketClient.cs | 7 ++++-- .../Net/DefaultWebSocketClientProvider.cs | 24 ++++++++++++------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index a54107829..637099fd6 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -22,7 +22,7 @@ namespace Discord.Net.Rest private CancellationToken _cancelToken; private bool _isDisposed; - public DefaultRestClient(string baseUrl) + public DefaultRestClient(string baseUrl, bool useProxy = false) { _baseUrl = baseUrl; @@ -30,7 +30,7 @@ namespace Discord.Net.Rest { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, - UseProxy = false + UseProxy = useProxy, }); SetHeader("accept-encoding", "gzip, deflate"); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs index 311a53562..e0e776549 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -4,16 +4,21 @@ namespace Discord.Net.Rest { public static class DefaultRestClientProvider { - public static readonly RestClientProvider Instance = url => + public static readonly RestClientProvider Instance = Create(); + + public static RestClientProvider Create(bool useProxy = false) { - try - { - return new DefaultRestClient(url); - } - catch (PlatformNotSupportedException ex) + return url => { - throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultRestClient(url, useProxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default RestClientProvider is not supported on this platform.", ex); + } + }; + } } } diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 282ae210a..a250acec9 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -23,18 +24,20 @@ namespace Discord.Net.WebSockets private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private ClientWebSocket _client; + private IWebProxy _proxy; private Task _task; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private bool _isDisposed, _isDisconnecting; - public DefaultWebSocketClient() + public DefaultWebSocketClient(IWebProxy proxy = null) { _lock = new SemaphoreSlim(1, 1); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _headers = new Dictionary(); + _proxy = proxy; } private void Dispose(bool disposing) { @@ -70,7 +73,7 @@ namespace Discord.Net.WebSockets _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; _client = new ClientWebSocket(); - _client.Options.Proxy = null; + _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = TimeSpan.Zero; foreach (var header in _headers) { diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 04b3f8388..68bd67c5b 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -1,21 +1,27 @@ using System; +using System.Net; namespace Discord.Net.WebSockets { public static class DefaultWebSocketProvider { #if DEFAULTWEBSOCKET - public static readonly WebSocketProvider Instance = () => + public static readonly WebSocketProvider Instance = Create(); + + public static WebSocketProvider Create(IWebProxy proxy = null) { - try - { - return new DefaultWebSocketClient(); - } - catch (PlatformNotSupportedException ex) + return () => { - throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); - } - }; + try + { + return new DefaultWebSocketClient(proxy); + } + catch (PlatformNotSupportedException ex) + { + throw new PlatformNotSupportedException("The default WebSocketProvider is not supported on this platform.", ex); + } + }; + } #else public static readonly WebSocketProvider Instance = () => { From 34b4e5a6d2f39a43ab3121f997cc3c92aca25165 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 23 Dec 2017 14:58:35 -0500 Subject: [PATCH 05/31] Refactor Games, support reading Rich Presences (#877) * Add API-level support for Rich Presences * Add library-level support for Game presences * Add model conversions for outgoing+incoming rich presences * Refactored Game into Activities * Integrated Activities with user entities rebase hell from 5f3cb947a92f4fd01cc4df47ca548180036b47f3 * Fix JSON converters for Activities * Finish rebase, activity should be set on BaseSocketClient * Use ApplicationId to define a rich presence * Added SetActivityAsync to Base and Sharded Socket clients * Remove public parameterless Game constructor * Remove GameAssets, refactored to GameAsset * Hide constructors for types that should be read-only * Revert changes to Discord.Net.sln got damned visual studio caching * Refactor GameParty to use dedicated current/capacity values Per feedback from @khionu --- src/Discord.Net.Core/CDN.cs | 6 ++ .../Entities/Activities/Game.cs | 19 +++++ .../Entities/Activities/GameAsset.cs | 15 ++++ .../Entities/Activities/GameParty.cs | 11 +++ .../Entities/Activities/GameSecrets.cs | 16 ++++ .../Entities/Activities/GameTimestamps.cs | 16 ++++ .../Entities/Activities/IActivity.cs | 13 +++ .../Entities/Activities/RichGame.cs | 22 +++++ .../Entities/Activities/StreamingGame.cs | 21 +++++ src/Discord.Net.Core/Entities/Users/Game.cs | 24 ------ .../Entities/Users/IPresence.cs | 4 +- src/Discord.Net.Rest/API/Common/Game.cs | 16 ++++ src/Discord.Net.Rest/API/Common/GameAssets.cs | 16 ++++ src/Discord.Net.Rest/API/Common/GameParty.cs | 12 +++ .../API/Common/GameSecrets.cs | 14 ++++ .../API/Common/GameTimestamps.cs | 15 ++++ .../API/UnixTimestampAttribute.cs | 7 ++ src/Discord.Net.Rest/Discord.Net.Rest.csproj | 3 +- .../Entities/Users/RestUser.cs | 2 +- .../Net/Converters/DiscordContractResolver.cs | 6 ++ .../Net/Converters/UnixTimestampConverter.cs | 28 +++++++ src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 3 +- .../DiscordShardedClient.cs | 13 ++- .../DiscordSocketClient.cs | 38 +++++---- .../Entities/Users/SocketPresence.cs | 10 +-- .../Entities/Users/SocketUser.cs | 2 +- .../Extensions/EntityExtensions.cs | 80 ++++++++++++++++++- 28 files changed, 376 insertions(+), 58 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/Game.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameAsset.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameParty.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameSecrets.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/IActivity.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/RichGame.cs create mode 100644 src/Discord.Net.Core/Entities/Activities/StreamingGame.cs delete mode 100644 src/Discord.Net.Core/Entities/Users/Game.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameAssets.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameParty.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameSecrets.cs create mode 100644 src/Discord.Net.Rest/API/Common/GameTimestamps.cs create mode 100644 src/Discord.Net.Rest/API/UnixTimestampAttribute.cs create mode 100644 src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d3ade3722..415c0c30d 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -22,6 +22,12 @@ namespace Discord public static string GetEmojiUrl(ulong emojiId) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) + { + string extension = FormatToExtension(format, ""); + return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; + } + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs new file mode 100644 index 000000000..f2b7e8eb6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Game : IActivity + { + public string Name { get; internal set; } + + internal Game() { } + public Game(string name) + { + Name = name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs new file mode 100644 index 000000000..385f37214 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class GameAsset + { + internal GameAsset() { } + + internal ulong ApplicationId { get; set; } + + public string Text { get; internal set; } + public string ImageId { get; internal set; } + + public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs new file mode 100644 index 000000000..dbfe5b6ce --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public class GameParty + { + internal GameParty() { } + + public string Id { get; internal set; } + public int Members { get; internal set; } + public int Capacity { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs new file mode 100644 index 000000000..e9d988ba9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class GameSecrets + { + public string Match { get; } + public string Join { get; } + public string Spectate { get; } + + internal GameSecrets(string match, string join, string spectate) + { + Match = match; + Join = join; + Spectate = spectate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs new file mode 100644 index 000000000..8c8c992fa --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class GameTimestamps + { + public DateTimeOffset? Start { get; } + public DateTimeOffset? End { get; } + + internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) + { + Start = start; + End = end; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs new file mode 100644 index 000000000..0dcf34273 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivity + { + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs new file mode 100644 index 000000000..e66eac1d2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RichGame : Game + { + internal RichGame() { } + + public string Details { get; internal set;} + public string State { get; internal set;} + public ulong ApplicationId { get; internal set; } + public GameAsset SmallAsset { get; internal set; } + public GameAsset LargeAsset { get; internal set; } + public GameParty Party { get; internal set; } + public GameSecrets Secrets { get; internal set; } + public GameTimestamps Timestamps { get; internal set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Rich)"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs new file mode 100644 index 000000000..140024272 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class StreamingGame : Game + { + public string Url { get; internal set; } + public StreamType StreamType { get; internal set; } + + public StreamingGame(string name, string url, StreamType streamType) + { + Name = name; + Url = url; + StreamType = streamType; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Url})"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs deleted file mode 100644 index 3405b0dd4..000000000 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Game - { - public string Name { get; } - public string StreamUrl { get; } - public StreamType StreamType { get; } - - public Game(string name, string streamUrl, StreamType type) - { - Name = name; - StreamUrl = streamUrl; - StreamType = type; - } - private Game(string name) - : this(name, null, StreamType.NotStreaming) { } - - public override string ToString() => Name; - private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; - } -} diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 7f182241b..25adcc9c4 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -2,8 +2,8 @@ { public interface IPresence { - /// Gets the game this user is currently playing, if any. - Game? Game { get; } + /// Gets the activity this user is currently doing. + IActivity Activity { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index a499d83b0..bfb861692 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -13,6 +13,22 @@ namespace Discord.API public Optional StreamUrl { get; set; } [JsonProperty("type")] public Optional StreamType { get; set; } + [JsonProperty("details")] + public Optional Details { get; set; } + [JsonProperty("state")] + public Optional State { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } + [JsonProperty("assets")] + public Optional Assets { get; set; } + [JsonProperty("party")] + public Optional Party { get; set; } + [JsonProperty("secrets")] + public Optional Secrets { get; set; } + [JsonProperty("timestamps")] + public Optional Timestamps { get; set; } + [JsonProperty("instance")] + public Optional Instance { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs new file mode 100644 index 000000000..b5928a8ab --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameAssets + { + [JsonProperty("small_text")] + public Optional SmallText { get; set; } + [JsonProperty("small_image")] + public Optional SmallImage { get; set; } + [JsonProperty("large_image")] + public Optional LargeText { get; set; } + [JsonProperty("large_text")] + public Optional LargeImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs new file mode 100644 index 000000000..e0da4a098 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameParty.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameParty + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("size")] + public int[] Size { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameSecrets.cs b/src/Discord.Net.Rest/API/Common/GameSecrets.cs new file mode 100644 index 000000000..e70b48ff0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameSecrets.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameSecrets + { + [JsonProperty("match")] + public string Match { get; set; } + [JsonProperty("join")] + public string Join { get; set; } + [JsonProperty("spectate")] + public string Spectate { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameTimestamps.cs b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs new file mode 100644 index 000000000..5c6f10b86 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameTimestamps + { + [JsonProperty("start")] + [UnixTimestamp] + public Optional Start { get; set; } + [JsonProperty("end")] + [UnixTimestamp] + public Optional End { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs new file mode 100644 index 000000000..3890ffc46 --- /dev/null +++ b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class UnixTimestampAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 439b7bbb1..29f79e410 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index d8ade3a6b..c6cf6103a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -16,7 +16,7 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index b465fbed2..9213c5d75 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -66,6 +66,12 @@ namespace Discord.Net.Converters if (type == typeof(ulong)) return UInt64Converter.Instance; } + bool hasUnixStamp = propInfo.GetCustomAttribute() != null; + if (hasUnixStamp) + { + if (type == typeof(DateTimeOffset)) + return UnixTimestampConverter.Instance; + } //Enums if (type == typeof(PermissionTarget)) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs new file mode 100644 index 000000000..d4660dc44 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -0,0 +1,28 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + public class UnixTimestampConverter : JsonConverter + { + public static readonly UnixTimestampConverter Instance = new UnixTimestampConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Discord doesn't validate if timestamps contain decimals or not + if (reader.Value is double d) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); + long offset = (long)reader.Value; + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index c6b0b2fd8..f55c83b75 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -18,7 +18,7 @@ namespace Discord.Rpc public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual bool IsWebhook => false; - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index d248285cd..2ab244aeb 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -13,7 +13,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public abstract int Latency { get; protected set; } public abstract UserStatus Status { get; protected set; } - public abstract Game? Game { get; protected set; } + public abstract IActivity Activity { get; protected set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; @@ -45,6 +45,7 @@ namespace Discord.WebSocket public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); /// diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 6c2a0f3b9..e827909d9 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -22,7 +22,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public override int Latency { get => GetLatency(); protected set { } } public override UserStatus Status { get => _shards[0].Status; protected set { } } - public override Game? Game { get => _shards[0].Game; protected set { } } + public override IActivity Activity { get => _shards[0].Activity; protected set { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); @@ -239,9 +239,18 @@ namespace Discord.WebSocket await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + { + IActivity activity = null; + if (streamUrl != null) + activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + activity = new Game(name); + await SetActivityAsync(activity).ConfigureAwait(false); + } + public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) - await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); + await _shards[i].SetActivityAsync(activity).ConfigureAwait(false); } private void RegisterEvents(DiscordSocketClient client, bool isPrimary) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d152bbc03..35a22edc4 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -48,7 +48,7 @@ namespace Discord.WebSocket /// public override int Latency { get; protected set; } public override UserStatus Status { get; protected set; } = UserStatus.Online; - public override Game? Game { get; protected set; } + public override IActivity Activity { get; protected set; } //From DiscordSocketConfig internal int TotalShards { get; private set; } @@ -328,33 +328,39 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (name != null) - Game = new Game(name, streamUrl, streamType); + if (streamUrl != null) + Activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + Activity = new Game(name); else - Game = null; + Activity = null; await SendStatusAsync().ConfigureAwait(false); } + public override async Task SetActivityAsync(IActivity activity) + { + Activity = activity; + await SendStatusAsync().ConfigureAwait(false); + } + private async Task SendStatusAsync() { if (CurrentUser == null) return; - var game = Game; + var activity = Activity; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, game); + CurrentUser.Presence = new SocketPresence(status, activity); - GameModel gameModel; - if (game != null) + var gameModel = new GameModel(); + // Discord only accepts rich presence over RPC, don't even bother building a payload + if (activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); + if (activity is StreamingGame stream) { - gameModel = new API.Game - { - Name = game.Value.Name, - StreamType = game.Value.StreamType, - StreamUrl = game.Value.StreamUrl - }; + gameModel.StreamUrl = stream.Url; + gameModel.StreamType = stream.StreamType; } - else - gameModel = null; + else if (activity != null) + gameModel.Name = activity.Name; await ApiClient.SendStatusUpdateAsync( status, diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 00d4b4bbc..7d7ba16ce 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -8,20 +8,20 @@ namespace Discord.WebSocket public struct SocketPresence : IPresence { public UserStatus Status { get; } - public Game? Game { get; } + public IActivity Activity { get; } - internal SocketPresence(UserStatus status, Game? game) + internal SocketPresence(UserStatus status, IActivity activity) { Status = status; - Game = game; + Activity= activity; } internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game != null ? model.Game.ToEntity() : (Game?)null); + return new SocketPresence(model.Status, model.Game?.ToEntity()); } public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index a0c78b93f..58d5c62a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,7 +18,7 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public Game? Game => Presence.Game; + public IActivity Activity => Presence.Activity; public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 636ef68f4..4aff13753 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -2,11 +2,83 @@ { internal static class EntityExtensions { - public static Game ToEntity(this API.Game model) + public static IActivity ToEntity(this API.Game model) { - return new Game(model.Name, - model.StreamUrl.GetValueOrDefault(null), - model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + // Rich Game + if (model.ApplicationId.IsSpecified) + { + ulong appId = model.ApplicationId.Value; + var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); + return new RichGame + { + ApplicationId = appId, + Name = model.Name, + Details = model.Details.GetValueOrDefault(), + State = model.State.GetValueOrDefault(), + SmallAsset = assets?[0], + LargeAsset = assets?[1], + Party = model.Party.GetValueOrDefault()?.ToEntity(), + Secrets = model.Secrets.GetValueOrDefault()?.ToEntity(), + Timestamps = model.Timestamps.GetValueOrDefault()?.ToEntity() + }; + } + // Stream Game + if (model.StreamUrl.IsSpecified) + { + return new StreamingGame( + model.Name, + model.StreamUrl.Value, + model.StreamType.Value.GetValueOrDefault()); + } + // Normal Game + return new Game(model.Name); + } + + // (Small, Large) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + { + return new GameAsset[] + { + model.SmallImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.SmallImage.GetValueOrDefault(), + Text = model.SmallText.GetValueOrDefault() + } : null, + model.LargeImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.LargeImage.GetValueOrDefault(), + Text = model.LargeText.GetValueOrDefault() + } : null, + }; + } + + public static GameParty ToEntity(this API.GameParty model) + { + // Discord will probably send bad data since they don't validate anything + int current = 0, cap = 0; + if (model.Size.Length == 2) + { + current = model.Size[0]; + cap = model.Size[1]; + } + return new GameParty + { + Id = model.Id, + Members = current, + Capacity = cap, + }; + } + + public static GameSecrets ToEntity(this API.GameSecrets model) + { + return new GameSecrets(model.Match, model.Join, model.Spectate); + } + + public static GameTimestamps ToEntity(this API.GameTimestamps model) + { + return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } } } From a19ff188e9f4ea7019966dd7411754230b739abc Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 23 Dec 2017 15:09:24 -0500 Subject: [PATCH 06/31] Added support for animated emoji (#913) * Added support for animated emoji This was such a useful feature Discord, I'm glad you added this instead of fixing bugs. * Fix bugs in emote parser * Added unit tests for emotes --- src/Discord.Net.Core/CDN.cs | 4 +- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 22 +++++++--- .../Entities/Emotes/GuildEmote.cs | 4 +- src/Discord.Net.Rest/API/Common/Emoji.cs | 2 + .../Entities/Messages/RestReaction.cs | 2 +- .../Extensions/EntityExtensions.cs | 2 +- .../Entities/Messages/SocketReaction.cs | 2 +- test/Discord.Net.Tests/Tests.Emotes.cs | 44 +++++++++++++++++++ 8 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 test/Discord.Net.Tests/Tests.Emotes.cs diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 415c0c30d..070b965ee 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -19,8 +19,8 @@ namespace Discord => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; - public static string GetEmojiUrl(ulong emojiId) - => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetEmojiUrl(ulong emojiId, bool animated) + => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index f498c818e..e3a228c83 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -16,13 +16,18 @@ namespace Discord /// The ID of this emote /// public ulong Id { get; } + /// + /// Is this emote animated? + /// + public bool Animated { get; } public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Url => CDN.GetEmojiUrl(Id); + public string Url => CDN.GetEmojiUrl(Id, Animated); - internal Emote(ulong id, string name) + internal Emote(ulong id, string name, bool animated) { Id = id; Name = name; + Animated = animated; } public override bool Equals(object other) @@ -59,17 +64,20 @@ namespace Discord public static bool TryParse(string text, out Emote result) { result = null; - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') { - int splitIndex = text.IndexOf(':', 2); + bool animated = text[1] == 'a'; + int startIndex = animated ? 3 : 2; + + int splitIndex = text.IndexOf(':', startIndex); if (splitIndex == -1) return false; if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) return false; - string name = text.Substring(2, splitIndex - 2); - result = new Emote(id, name); + string name = text.Substring(startIndex, splitIndex - startIndex); + result = new Emote(id, name, animated); return true; } return false; @@ -77,6 +85,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 8d776a4cd..95b062bd2 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -13,7 +13,7 @@ namespace Discord public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) + internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) { IsManaged = isManaged; RequireColons = requireColons; @@ -21,6 +21,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => $"<:{Name}:{Id}>"; + public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Rest/API/Common/Emoji.cs b/src/Discord.Net.Rest/API/Common/Emoji.cs index bd9c4d466..2bdfdcc36 100644 --- a/src/Discord.Net.Rest/API/Common/Emoji.cs +++ b/src/Discord.Net.Rest/API/Common/Emoji.cs @@ -9,6 +9,8 @@ namespace Discord.API public ulong? Id { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("animated")] + public bool? Animated { get; set; } [JsonProperty("roles")] public ulong[] Roles { get; set; } [JsonProperty("require_colons")] diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 05c817935..6d3f72419 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -18,7 +18,7 @@ namespace Discord.Rest { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new RestReaction(emote, model.Count, model.Me); diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index b88a5b515..74b05dacd 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -7,7 +7,7 @@ namespace Discord.Rest { public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Animated.GetValueOrDefault(), model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 35bee9e68..e8fa17a35 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -24,7 +24,7 @@ namespace Discord.WebSocket { IEmote emote; if (model.Emoji.Id.HasValue) - emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name, model.Emoji.Animated.GetValueOrDefault()); else emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); diff --git a/test/Discord.Net.Tests/Tests.Emotes.cs b/test/Discord.Net.Tests/Tests.Emotes.cs new file mode 100644 index 000000000..334975ce4 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Emotes.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; + +namespace Discord +{ + public class EmoteTests + { + [Fact] + public void Test_Emote_Parse() + { + Assert.True(Emote.TryParse("<:typingstatus:394207658351263745>", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.False(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("png", emote.Url); + } + [Fact] + public void Test_Invalid_Emote_Parse() + { + Assert.False(Emote.TryParse("invalid", out _)); + Assert.False(Emote.TryParse("<:typingstatus:not_a_number>", out _)); + Assert.Throws(() => Emote.Parse("invalid")); + } + [Fact] + public void Test_Animated_Emote_Parse() + { + Assert.True(Emote.TryParse("", out Emote emote)); + Assert.NotNull(emote); + Assert.Equal("typingstatus", emote.Name); + Assert.Equal(394207658351263745UL, emote.Id); + Assert.True(emote.Animated); + Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1514056829775), emote.CreatedAt); + Assert.EndsWith("gif", emote.Url); + } + public void Test_Invalid_Amimated_Emote_Parse() + { + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + Assert.False(Emote.TryParse("", out _)); + } + } +} From 7b2ddd027cd5f2fed37a06364f827ebe36fc5dbe Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sat, 23 Dec 2017 15:17:20 -0500 Subject: [PATCH 07/31] Add missing REST Webhook implemenation (#843) * Add Webhook API models, REST implementation, and Socket bridges. * Remove token overrides from REST. Leaving that as a Webhook package only feature. * Add Webhook API models, REST implementation, and Socket bridges. * Remove token overrides from REST. Leaving that as a Webhook package only feature. * Webhook core implementation. * Webhook REST implementation. * Webhook client implementation. * Add channel bucket id. --- .../Entities/Channels/ITextChannel.cs | 8 ++ .../Entities/Guilds/IGuild.cs | 5 + .../Entities/Users/IWebhookUser.cs | 1 - .../Entities/Webhooks/IWebhook.cs | 34 +++++++ .../Entities/Webhooks/WebhookProperties.cs | 41 +++++++++ src/Discord.Net.Core/IDiscordClient.cs | 2 + src/Discord.Net.Rest/API/Common/Webhook.cs | 25 +++++ .../API/Rest/CreateWebhookParams.cs | 14 +++ .../API/Rest/ModifyWebhookParams.cs | 16 ++++ .../API/Rest/UploadWebhookFileParams.cs | 6 +- src/Discord.Net.Rest/BaseDiscordClient.cs | 3 + src/Discord.Net.Rest/ClientHelper.cs | 8 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 74 ++++++++++++++- src/Discord.Net.Rest/DiscordRestClient.cs | 6 ++ .../Entities/Channels/ChannelHelper.cs | 25 +++++ .../Entities/Channels/RestTextChannel.cs | 15 +++ .../Entities/Guilds/GuildHelper.cs | 14 +++ .../Entities/Guilds/RestGuild.cs | 11 +++ .../Entities/Webhooks/RestWebhook.cs | 91 +++++++++++++++++++ .../Entities/Webhooks/WebhookHelper.cs | 38 ++++++++ .../Entities/Channels/RpcTextChannel.cs | 16 +++- .../API/Gateway/WebhookUpdateEvent.cs | 13 +++ .../Entities/Channels/SocketTextChannel.cs | 18 +++- .../Entities/Guilds/SocketGuild.cs | 11 +++ .../DiscordWebhookClient.cs | 77 +++++++++------- .../Entities/Webhooks/RestInternalWebhook.cs | 66 ++++++++++++++ .../WebhookClientHelper.cs | 81 +++++++++++++++++ 27 files changed, 679 insertions(+), 40 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs create mode 100644 src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs create mode 100644 src/Discord.Net.Rest/API/Common/Webhook.cs create mode 100644 src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs create mode 100644 src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs create mode 100644 src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs create mode 100644 src/Discord.Net.Webhook/WebhookClientHelper.cs diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index be4dd0260..7c6ec3908 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace Discord @@ -19,5 +20,12 @@ namespace Discord /// Modifies this text channel. Task ModifyAsync(Action func, RequestOptions options = null); + + /// Creates a webhook in this text channel. + Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); + /// Gets the webhook in this text channel with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets the webhooks for this text channel. + Task> GetWebhooksAsync(RequestOptions options = null); } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 6b2d24cc6..0fdd92405 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -118,6 +118,11 @@ namespace Discord /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + /// Gets the webhook in this guild with the provided id, or null if not found. + Task GetWebhookAsync(ulong id, RequestOptions options = null); + /// Gets a collection of all webhooks for this guild. + Task> GetWebhooksAsync(RequestOptions options = null); + /// Gets a specific emote from this guild. Task GetEmoteAsync(ulong id, RequestOptions options = null); /// Creates a new emote in this guild. diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index 8f4d42187..be769b944 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,6 +1,5 @@ namespace Discord { - //TODO: Add webhook endpoints public interface IWebhookUser : IGuildUser { ulong WebhookId { get; } diff --git a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs new file mode 100644 index 000000000..ef56f72b9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IWebhook : IDeletable, ISnowflakeEntity + { + /// Gets the token of this webhook. + string Token { get; } + + /// Gets the default name of this webhook. + string Name { get; } + /// Gets the id of this webhook's default avatar. + string AvatarId { get; } + /// Gets the url to this webhook's default avatar. + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + + /// Gets the channel for this webhook. + ITextChannel Channel { get; } + /// Gets the id of the channel for this webhook. + ulong ChannelId { get; } + + /// Gets the guild owning this webhook. + IGuild Guild { get; } + /// Gets the id of the guild owning this webhook. + ulong? GuildId { get; } + + /// Gets the user that created this webhook. + IUser Creator { get; } + + /// Modifies this webhook. + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs new file mode 100644 index 000000000..8759a1729 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -0,0 +1,41 @@ +namespace Discord +{ + /// + /// Modify an with the specified parameters. + /// + /// + /// + /// await webhook.ModifyAsync(x => + /// { + /// x.Name = "Bob"; + /// x.Avatar = new Image("avatar.jpg"); + /// }); + /// + /// + /// + public class WebhookProperties + { + /// + /// The default name of the webhook. + /// + public Optional Name { get; set; } + /// + /// The default avatar of the webhook. + /// + public Optional Image { get; set; } + /// + /// The channel for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional Channel { get; set; } + /// + /// The channel id for this webhook. + /// + /// + /// This field is not used when authenticated with . + /// + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 23e8e9c5b..9abb959b5 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -34,5 +34,7 @@ namespace Discord Task> GetVoiceRegionsAsync(RequestOptions options = null); Task GetVoiceRegionAsync(string id, RequestOptions options = null); + + Task GetWebhookAsync(ulong id, RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Common/Webhook.cs b/src/Discord.Net.Rest/API/Common/Webhook.cs new file mode 100644 index 000000000..cbd5fdad5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Webhook.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Webhook + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + + [JsonProperty("user")] + public Optional Creator { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs new file mode 100644 index 000000000..0d1059fab --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookParams.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookParams + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs new file mode 100644 index 000000000..0f2d6e33b --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index f2c34c015..6d6eb29b2 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 -using Discord.Net.Rest; using System.Collections.Generic; using System.IO; +using Discord.Net.Rest; namespace Discord.API.Rest { @@ -15,6 +15,7 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } public Optional Username { get; set; } public Optional AvatarUrl { get; set; } + public Optional Embeds { get; set; } public UploadWebhookFileParams(Stream file) { @@ -25,6 +26,7 @@ namespace Discord.API.Rest { var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) d["content"] = Content.Value; if (IsTTS.IsSpecified) @@ -35,6 +37,8 @@ namespace Discord.API.Rest d["username"] = Username.Value; if (AvatarUrl.IsSpecified) d["avatar_url"] = AvatarUrl.Value; + if (Embeds.IsSpecified) + d["embeds"] = Embeds.Value; return d; } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 47a946f20..269dedd71 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -164,6 +164,9 @@ namespace Discord.Rest Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + Task IDiscordClient.StartAsync() => Task.Delay(0); Task IDiscordClient.StopAsync() diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 2f05d5d36..26d8c720e 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -144,6 +144,14 @@ namespace Discord.Rest return null; } + public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id); + if (model != null) + return RestWebhook.Create(client, (IGuild)null, model); + return null; + } + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 4e65b19d2..ab47b1e98 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -473,7 +473,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -486,8 +486,8 @@ namespace Discord.API 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)); options = RequestOptions.CreateOrClone(options); - - await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { @@ -503,7 +503,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -522,7 +522,7 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -1198,6 +1198,70 @@ namespace Discord.API return await SendAsync>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false); } + //Webhooks + public async Task CreateWebhookAsync(ulong channelId, CreateWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNull(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + var ids = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options); + } + public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + try + { + if (AuthTokenType == TokenType.Webhook) + return await SendAsync("GET", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendAsync("GET", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + public async Task ModifyWebhookAsync(ulong webhookId, ModifyWebhookParams args, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), options: options).ConfigureAwait(false); + else + return await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}", args, new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task DeleteWebhookAsync(ulong webhookId, RequestOptions options = null) + { + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (AuthTokenType == TokenType.Webhook) + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}", new BucketIds(), options: options).ConfigureAwait(false); + else + await SendAsync("DELETE", () => $"webhooks/{webhookId}", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task> GetGuildWebhooksAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/webhooks", ids, options: options).ConfigureAwait(false); + } + public async Task> GetChannelWebhooksAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + return await SendAsync>("GET", () => $"channels/{channelId}/webhooks", ids, options: options).ConfigureAwait(false); + } + //Helpers protected void CheckState() { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index aa9937008..3d90b6c00 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -91,6 +91,9 @@ namespace Discord.Rest /// public Task GetVoiceRegionAsync(string id, RequestOptions options = null) => ClientHelper.GetVoiceRegionAsync(this, id, options); + /// + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetWebhookAsync(this, id, options); //IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) @@ -160,5 +163,8 @@ namespace Discord.Rest => await GetVoiceRegionsAsync(options).ConfigureAwait(false); async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + + async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 8dcb8c284..fa870be17 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; using UserModel = Discord.API.User; +using WebhookModel = Discord.API.Webhook; namespace Discord.Rest { @@ -280,6 +281,30 @@ namespace Discord.Rest RequestOptions options) => new TypingNotifier(client, channel, options); + //Webhooks + public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) + { + var args = new CreateWebhookParams { Name = name }; + if (avatar != null) + args.Avatar = new API.Image(avatar); + + var model = await client.ApiClient.CreateWebhookAsync(channel.Id, args, options).ConfigureAwait(false); + return RestWebhook.Create(client, channel, model); + } + public static async Task GetWebhookAsync(ITextChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, channel, model); + } + public static async Task> GetWebhooksAsync(ITextChannel channel, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetChannelWebhooksAsync(channel.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, channel, x)) + .ToImmutableArray(); + } + //Helpers private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 8a096302b..9c29624c1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -77,8 +77,23 @@ namespace Discord.Rest public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 58b7ed7f9..7e25b53f3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -254,6 +254,20 @@ namespace Discord.Rest return model.Pruned; } + //Webhooks + public static async Task GetWebhookAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) + { + var model = await client.ApiClient.GetWebhookAsync(id, options: options).ConfigureAwait(false); + if (model == null) + return null; + return RestWebhook.Create(client, guild, model); + } + public static async Task> GetWebhooksAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildWebhooksAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestWebhook.Create(client, guild, x)).ToImmutableArray(); + } + //Emotes public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index de4b89e39..3b6a2bfa8 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -257,6 +257,12 @@ namespace Discord.Rest public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; @@ -396,5 +402,10 @@ namespace Discord.Rest return ImmutableArray.Create(); } Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs new file mode 100644 index 000000000..47cc50a9c --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhook : RestEntity, IWebhook, IUpdateable + { + internal IGuild Guild { get; private set; } + internal ITextChannel Channel { get; private set; } + + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + public IUser Creator { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) + : base(discord, id) + { + Guild = guild; + Token = token; + ChannelId = channelId; + } + internal RestWebhook(BaseDiscordClient discord, ITextChannel channel, ulong id, string token, ulong channelId) + : this(discord, channel.Guild, id, token, channelId) + { + Channel = channel; + } + + internal static RestWebhook Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestWebhook(discord, guild, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + internal static RestWebhook Create(BaseDiscordClient discord, ITextChannel channel, Model model) + { + var entity = new RestWebhook(discord, channel, model.Id, model.Token, model.ChannelId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.Creator.IsSpecified) + Creator = RestUser.Create(Discord, model.Creator.Value); + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public async Task UpdateAsync(RequestOptions options = null) + { + var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); + Update(model); + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookHelper.DeleteAsync(this, Discord, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + //IWebhook + IGuild IWebhook.Guild + => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + ITextChannel IWebhook.Channel + => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + Task IWebhook.ModifyAsync(Action func, RequestOptions options) + => ModifyAsync(func, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs new file mode 100644 index 000000000..50e9cab78 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Webhooks/WebhookHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Discord.API.Rest; +using ImageModel = Discord.API.Image; +using Model = Discord.API.Webhook; + +namespace Discord.Rest +{ + internal static class WebhookHelper + { + public static async Task ModifyAsync(IWebhook webhook, BaseDiscordClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(webhook.AvatarId); + + if (args.Channel.IsSpecified) + apiArgs.ChannelId = args.Channel.Value.Id; + else if (args.ChannelId.IsSpecified) + apiArgs.ChannelId = args.ChannelId.Value; + + return await client.ApiClient.ModifyWebhookAsync(webhook.Id, apiArgs, options).ConfigureAwait(false); + } + public static async Task DeleteAsync(IWebhook webhook, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(webhook.Id, options).ConfigureAwait(false); + } + + } +} diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 9de2968db..8c49f0671 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -68,11 +68,25 @@ namespace Discord.Rpc => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; //ITextChannel string ITextChannel.Topic { get { throw new NotSupportedException(); } } + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs new file mode 100644 index 000000000..e5c7afe41 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/WebhookUpdateEvent.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class WebhookUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 07ec630d3..b6a304b50 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -112,10 +112,26 @@ namespace Discord.WebSocket } return null; } - + + //Webhooks + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => ChannelHelper.GetWebhooksAsync(this, Discord, options); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; + //ITextChannel + async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => await CreateWebhookAsync(name, avatar, options); + async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); + //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index b639a9cf7..c4158c136 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -433,6 +433,12 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + //Webhooks + public Task GetWebhookAsync(ulong id, RequestOptions options = null) + => GuildHelper.GetWebhookAsync(this, Discord, id, options); + public Task> GetWebhooksAsync(RequestOptions options = null) + => GuildHelper.GetWebhooksAsync(this, Discord, options); + //Emotes public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); @@ -682,5 +688,10 @@ namespace Discord.WebSocket Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + + async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) + => await GetWebhookAsync(id, options); + async Task> IGuild.GetWebhooksAsync(RequestOptions options) + => await GetWebhooksAsync(options); } } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 3d8307da4..59cc8f3e7 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,32 +1,49 @@ -using Discord.API.Rest; -using Discord.Rest; using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using System.Linq; using Discord.Logging; +using Discord.Rest; namespace Discord.Webhook { - public partial class DiscordWebhookClient + public class DiscordWebhookClient : IDisposable { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); private readonly ulong _webhookId; + internal IWebhook Webhook; internal readonly Logger _restLogger; internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook) + : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken) : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + : this(config) { _webhookId = webhookId; + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); + } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) + : this(config) + { + Webhook = webhook; + _webhookId = Webhook.Id; + } + private DiscordWebhookClient(DiscordRestConfig config) + { ApiClient = CreateApiClient(config); LogManager = new LogManager(config.LogLevel); LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); @@ -41,42 +58,40 @@ namespace Discord.Webhook await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - - public async Task SendMessageAsync(string text, bool isTTS = false, Embed[] embeds = null, + + /// Sends a message using to the channel for this webhook. Returns the ID of the created message. + public Task SendMessageAsync(string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) - { - var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; - if (embeds != null) - args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = avatarUrl; - await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); - } + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); #if FILESYSTEM - public async Task SendFileAsync(string filePath, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(string filePath, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options); +#endif + /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options); + + /// Modifies the properties of this webhook. + public Task ModifyWebhookAsync(Action func, RequestOptions options = null) + => Webhook.ModifyAsync(func, options); + + /// Deletes this webhook from Discord and disposes the client. + public async Task DeleteWebhookAsync(RequestOptions options = null) { - string filename = Path.GetFileName(filePath); - using (var file = File.OpenRead(filePath)) - await SendFileAsync(file, filename, text, isTTS, username, avatarUrl, options).ConfigureAwait(false); + await Webhook.DeleteAsync(options).ConfigureAwait(false); + Dispose(); } -#endif - public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, - string username = null, string avatarUrl = null, RequestOptions options = null) + + public void Dispose() { - var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = username; - await ApiClient.UploadWebhookFileAsync(_webhookId, args, options).ConfigureAwait(false); + ApiClient?.Dispose(); } } } diff --git a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs new file mode 100644 index 000000000..cd35d731c --- /dev/null +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Webhook; + +namespace Discord.Webhook +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + internal class RestInternalWebhook : IWebhook + { + private DiscordWebhookClient _client; + + public ulong Id { get; } + public ulong ChannelId { get; } + public string Token { get; } + + public string Name { get; private set; } + public string AvatarId { get; private set; } + public ulong? GuildId { get; private set; } + + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal RestInternalWebhook(DiscordWebhookClient apiClient, Model model) + { + _client = apiClient; + Id = model.Id; + ChannelId = model.Id; + Token = model.Token; + } + internal static RestInternalWebhook Create(DiscordWebhookClient client, Model model) + { + var entity = new RestInternalWebhook(client, model); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + if (model.Avatar.IsSpecified) + AvatarId = model.Avatar.Value; + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + if (model.Name.IsSpecified) + Name = model.Name.Value; + } + + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await WebhookClientHelper.ModifyAsync(_client, func, options); + Update(model); + } + + public Task DeleteAsync(RequestOptions options = null) + => WebhookClientHelper.DeleteAsync(_client, options); + + public override string ToString() => $"Webhook: {Name}:{Id}"; + private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; + + IUser IWebhook.Creator => null; + ITextChannel IWebhook.Channel => null; + IGuild IWebhook.Guild => null; + } +} diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs new file mode 100644 index 000000000..f3a3984cf --- /dev/null +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Discord.API.Rest; +using Discord.Rest; +using ImageModel = Discord.API.Image; +using WebhookModel = Discord.API.Webhook; + +namespace Discord.Webhook +{ + internal static class WebhookClientHelper + { + public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) + { + var model = await client.ApiClient.GetWebhookAsync(webhookId); + if (model == null) + throw new InvalidOperationException("Could not find a webhook for the supplied credentials."); + return RestInternalWebhook.Create(client, model); + } + public static async Task SendMessageAsync(DiscordWebhookClient client, + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = avatarUrl; + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + return model.Id; + } +#if FILESYSTEM + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options).ConfigureAwait(false); + } +#endif + public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + return msg.Id; + } + + public static async Task ModifyAsync(DiscordWebhookClient client, + Action func, RequestOptions options) + { + var args = new WebhookProperties(); + func(args); + var apiArgs = new ModifyWebhookParams + { + Avatar = args.Image.IsSpecified ? args.Image.Value?.ToModel() : Optional.Create(), + Name = args.Name + }; + + if (!apiArgs.Avatar.IsSpecified && client.Webhook.AvatarId != null) + apiArgs.Avatar = new ImageModel(client.Webhook.AvatarId); + + return await client.ApiClient.ModifyWebhookAsync(client.Webhook.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task DeleteAsync(DiscordWebhookClient client, RequestOptions options) + { + await client.ApiClient.DeleteWebhookAsync(client.Webhook.Id, options).ConfigureAwait(false); + } + } +} From 5ce85deb9db694de28333bd3b5655be1638bcddd Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 27 Dec 2017 14:47:55 -0500 Subject: [PATCH 08/31] Attempt to patch the GameParty nullref Not sure if this works, needs a more proper solution in the future anyways --- src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 4aff13753..b59702be2 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -56,6 +56,8 @@ public static GameParty ToEntity(this API.GameParty model) { + // todo: proper fix for this + if (model == null) return null; // Discord will probably send bad data since they don't validate anything int current = 0, cap = 0; if (model.Size.Length == 2) From 5f46aef3a761b24dcd8ef5ecb4b8811d0f374059 Mon Sep 17 00:00:00 2001 From: vim2meta Date: Fri, 5 Jan 2018 15:02:27 -0500 Subject: [PATCH 09/31] Ability to ignore unused parameters instead of failing the command. (#915) * Addition of FailOnTooManyArgs * Correct name & only pass in bool --- src/Discord.Net.Commands/CommandParser.cs | 9 +++++++-- src/Discord.Net.Commands/CommandService.cs | 3 ++- src/Discord.Net.Commands/CommandServiceConfig.cs | 3 +++ src/Discord.Net.Commands/Info/CommandInfo.cs | 4 +++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 28e36d54d..d65d99349 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -14,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) + public static async Task ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -109,7 +109,12 @@ namespace Discord.Commands if (argString != null) { if (curParam == null) - return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + { + if (ignoreExtraArgs) + break; + else + return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); + } var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index cf2b93277..8e7dab898 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -27,7 +27,7 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; - internal readonly bool _caseSensitive, _throwOnError; + internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; internal readonly Logger _cmdLogger; @@ -42,6 +42,7 @@ namespace Discord.Commands { _caseSensitive = config.CaseSensitiveCommands; _throwOnError = config.ThrowOnError; + _ignoreExtraArgs = config.IgnoreExtraArgs; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index b53b0248c..7fdbe368b 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -15,5 +15,8 @@ /// Determines whether RunMode.Sync commands should push exceptions up to the caller. public bool ThrowOnError { get; set; } = true; + + /// Determines whether extra parameters should be ignored. + public bool IgnoreExtraArgs { get; set; } = false; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 6bb621f94..f0d406e8d 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -18,6 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + private readonly CommandService _commandService; private readonly Func _action; public ModuleInfo Module { get; } @@ -64,6 +65,7 @@ namespace Discord.Commands HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; _action = builder.Callback; + _commandService = service; } public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) @@ -117,7 +119,7 @@ namespace Discord.Commands return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgsAsync(this, context, services, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) From b30af57b7facbac43f1079891b249471d67e9983 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 5 Jan 2018 20:23:19 -0500 Subject: [PATCH 10/31] Remove RPC from main distribution (#925) --- Discord.Net.sln | 17 +---------------- appveyor.yml | 1 - .../API/Rpc/AuthenticateParams.cs | 0 .../API/Rpc/AuthenticateResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs | 0 .../API/Rpc/AuthorizeResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/Channel.cs | 0 .../API/Rpc/ChannelSubscriptionParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/ChannelSummary.cs | 0 .../Discord.Net.Rpc/API/Rpc/ErrorEvent.cs | 0 .../API/Rpc/ExtendedVoiceState.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetChannelParams.cs | 0 .../API/Rpc/GetChannelsParams.cs | 0 .../API/Rpc/GetChannelsResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetGuildParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs | 0 .../API/Rpc/GetGuildsResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/Guild.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildMember.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs | 0 .../API/Rpc/GuildSubscriptionParams.cs | 0 .../Discord.Net.Rpc/API/Rpc/GuildSummary.cs | 0 .../Discord.Net.Rpc/API/Rpc/Message.cs | 0 .../Discord.Net.Rpc/API/Rpc/MessageEvent.cs | 0 .../Discord.Net.Rpc/API/Rpc/Pan.cs | 0 .../Discord.Net.Rpc/API/Rpc/ReadyEvent.cs | 0 .../Discord.Net.Rpc/API/Rpc/RpcConfig.cs | 0 .../API/Rpc/SelectChannelParams.cs | 0 .../API/Rpc/SetLocalVolumeParams.cs | 0 .../API/Rpc/SetLocalVolumeResponse.cs | 0 .../Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs | 0 .../API/Rpc/SubscriptionResponse.cs | 0 .../API/Rpc/UserVoiceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceDevice.cs | 0 .../API/Rpc/VoiceDeviceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceMode.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceSettings.cs | 0 .../Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs | 0 .../Discord.Net.Rpc/API/RpcFrame.cs | 0 .../Discord.Net.Rpc/AssemblyInfo.cs | 0 .../Commands/RpcCommandContext.cs | 0 .../Discord.Net.Rpc/Discord.Net.Rpc.csproj | 0 .../Discord.Net.Rpc/DiscordRpcApiClient.cs | 0 .../Discord.Net.Rpc/DiscordRpcClient.Events.cs | 0 .../Discord.Net.Rpc/DiscordRpcClient.cs | 0 .../Discord.Net.Rpc/DiscordRpcConfig.cs | 0 .../Entities/Channels/IRpcAudioChannel.cs | 0 .../Entities/Channels/IRpcMessageChannel.cs | 0 .../Entities/Channels/IRpcPrivateChannel.cs | 0 .../Entities/Channels/RpcChannel.cs | 0 .../Entities/Channels/RpcChannelSummary.cs | 0 .../Entities/Channels/RpcDMChannel.cs | 0 .../Entities/Channels/RpcGroupChannel.cs | 0 .../Entities/Channels/RpcGuildChannel.cs | 0 .../Entities/Channels/RpcTextChannel.cs | 0 .../Entities/Channels/RpcVoiceChannel.cs | 0 .../Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs | 0 .../Entities/Guilds/RpcGuildStatus.cs | 0 .../Entities/Guilds/RpcGuildSummary.cs | 0 .../Entities/Messages/RpcMessage.cs | 0 .../Entities/Messages/RpcSystemMessage.cs | 0 .../Entities/Messages/RpcUserMessage.cs | 0 .../Discord.Net.Rpc/Entities/RpcEntity.cs | 0 .../Entities/UserVoiceProperties.cs | 0 .../Discord.Net.Rpc/Entities/Users/Pan.cs | 0 .../Entities/Users/RpcGuildUser.cs | 0 .../Discord.Net.Rpc/Entities/Users/RpcUser.cs | 0 .../Entities/Users/RpcVoiceState.cs | 0 .../Entities/Users/RpcWebhookUser.cs | 0 .../Discord.Net.Rpc/Entities/VoiceDevice.cs | 0 .../Entities/VoiceDeviceProperties.cs | 0 .../Entities/VoiceModeProperties.cs | 0 .../Discord.Net.Rpc/Entities/VoiceProperties.cs | 0 .../Discord.Net.Rpc/Entities/VoiceSettings.cs | 0 .../Discord.Net.Rpc/Entities/VoiceShortcut.cs | 0 .../Entities/VoiceShortcutType.cs | 0 .../Extensions/EntityExtensions.cs | 0 .../Discord.Net.Rpc/RpcChannelEvent.cs | 0 .../Discord.Net.Rpc/RpcGlobalEvent.cs | 0 .../Discord.Net.Rpc/RpcGuildEvent.cs | 0 src/Discord.Net/Discord.Net.nuspec | 3 --- 81 files changed, 1 insertion(+), 20 deletions(-) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Channel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Guild.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildMember.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/GuildSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Message.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/MessageEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/Pan.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/RpcConfig.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceMode.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs (100%) rename {src => experiment}/Discord.Net.Rpc/API/RpcFrame.cs (100%) rename {src => experiment}/Discord.Net.Rpc/AssemblyInfo.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Commands/RpcCommandContext.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Discord.Net.Rpc.csproj (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcApiClient.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcClient.Events.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcClient.cs (100%) rename {src => experiment}/Discord.Net.Rpc/DiscordRpcConfig.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/RpcEntity.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/UserVoiceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/Pan.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceDevice.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceModeProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceProperties.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceSettings.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceShortcut.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Entities/VoiceShortcutType.cs (100%) rename {src => experiment}/Discord.Net.Rpc/Extensions/EntityExtensions.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcChannelEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcGlobalEvent.cs (100%) rename {src => experiment}/Discord.Net.Rpc/RpcGuildEvent.cs (100%) diff --git a/Discord.Net.sln b/Discord.Net.sln index 58bfcad86..cac6c9064 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.csproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.csproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" @@ -58,18 +56,6 @@ Global {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -136,7 +122,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} - {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} diff --git a/appveyor.yml b/appveyor.yml index 3bf70c09c..393485fee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,6 @@ after_build: - ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthenticateResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/AuthorizeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Channel.cs b/experiment/Discord.Net.Rpc/API/Rpc/Channel.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Channel.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Channel.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ChannelSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ErrorEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs b/experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ExtendedVoiceState.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetChannelsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GetGuildsResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Guild.cs b/experiment/Discord.Net.Rpc/API/Rpc/Guild.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Guild.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Guild.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildMember.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildMember.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildMember.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildStatusEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSubscriptionParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs b/experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/GuildSummary.cs rename to experiment/Discord.Net.Rpc/API/Rpc/GuildSummary.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Message.cs b/experiment/Discord.Net.Rpc/API/Rpc/Message.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Message.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Message.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/MessageEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/MessageEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/Pan.cs b/experiment/Discord.Net.Rpc/API/Rpc/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/Pan.cs rename to experiment/Discord.Net.Rpc/API/Rpc/Pan.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/ReadyEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs b/experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/RpcConfig.cs rename to experiment/Discord.Net.Rpc/API/Rpc/RpcConfig.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SelectChannelParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeParams.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SetLocalVolumeResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs b/experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SpeakingEvent.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs b/experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs rename to experiment/Discord.Net.Rpc/API/Rpc/SubscriptionResponse.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/UserVoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceDeviceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceMode.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceMode.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/API/Rpc/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/API/RpcFrame.cs b/experiment/Discord.Net.Rpc/API/RpcFrame.cs similarity index 100% rename from src/Discord.Net.Rpc/API/RpcFrame.cs rename to experiment/Discord.Net.Rpc/API/RpcFrame.cs diff --git a/src/Discord.Net.Rpc/AssemblyInfo.cs b/experiment/Discord.Net.Rpc/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Rpc/AssemblyInfo.cs rename to experiment/Discord.Net.Rpc/AssemblyInfo.cs diff --git a/src/Discord.Net.Rpc/Commands/RpcCommandContext.cs b/experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs similarity index 100% rename from src/Discord.Net.Rpc/Commands/RpcCommandContext.cs rename to experiment/Discord.Net.Rpc/Commands/RpcCommandContext.cs diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj similarity index 100% rename from src/Discord.Net.Rpc/Discord.Net.Rpc.csproj rename to experiment/Discord.Net.Rpc/Discord.Net.Rpc.csproj diff --git a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcApiClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.Events.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.Events.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.Events.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcClient.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcClient.cs rename to experiment/Discord.Net.Rpc/DiscordRpcClient.cs diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs similarity index 100% rename from src/Discord.Net.Rpc/DiscordRpcConfig.cs rename to experiment/Discord.Net.Rpc/DiscordRpcConfig.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcAudioChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcMessageChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/IRpcPrivateChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs rename to experiment/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuild.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildStatus.cs diff --git a/src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs b/experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs rename to experiment/Discord.Net.Rpc/Entities/Guilds/RpcGuildSummary.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs rename to experiment/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/experiment/Discord.Net.Rpc/Entities/RpcEntity.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/RpcEntity.cs rename to experiment/Discord.Net.Rpc/Entities/RpcEntity.cs diff --git a/src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/UserVoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/UserVoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/Pan.cs b/experiment/Discord.Net.Rpc/Entities/Users/Pan.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/Pan.cs rename to experiment/Discord.Net.Rpc/Entities/Users/Pan.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcGuildUser.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcUser.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcVoiceState.cs diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs rename to experiment/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDevice.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDevice.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDevice.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceDeviceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceModeProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceModeProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceProperties.cs b/experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceProperties.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceProperties.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceSettings.cs b/experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceSettings.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceSettings.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcut.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcut.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcut.cs diff --git a/src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs b/experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs similarity index 100% rename from src/Discord.Net.Rpc/Entities/VoiceShortcutType.cs rename to experiment/Discord.Net.Rpc/Entities/VoiceShortcutType.cs diff --git a/src/Discord.Net.Rpc/Extensions/EntityExtensions.cs b/experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs similarity index 100% rename from src/Discord.Net.Rpc/Extensions/EntityExtensions.cs rename to experiment/Discord.Net.Rpc/Extensions/EntityExtensions.cs diff --git a/src/Discord.Net.Rpc/RpcChannelEvent.cs b/experiment/Discord.Net.Rpc/RpcChannelEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcChannelEvent.cs rename to experiment/Discord.Net.Rpc/RpcChannelEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGlobalEvent.cs b/experiment/Discord.Net.Rpc/RpcGlobalEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGlobalEvent.cs rename to experiment/Discord.Net.Rpc/RpcGlobalEvent.cs diff --git a/src/Discord.Net.Rpc/RpcGuildEvent.cs b/experiment/Discord.Net.Rpc/RpcGuildEvent.cs similarity index 100% rename from src/Discord.Net.Rpc/RpcGuildEvent.cs rename to experiment/Discord.Net.Rpc/RpcGuildEvent.cs diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index f904f4126..cd57d2fcf 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -16,7 +16,6 @@ - @@ -24,7 +23,6 @@ - @@ -32,7 +30,6 @@ - From 227f61aa4e05affb0eb76aa3ab2ebd3bc2c0e30f Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 5 Jan 2018 17:24:21 -0800 Subject: [PATCH 11/31] Allow null value to reset IGuildUser nickname (#923) * Added workaround for UserHelper#ModifyAsync that accepts null values as a way to reset user nicknames * Update comments * Update comment to use see tag --- .../Entities/Users/GuildUserProperties.cs | 2 +- src/Discord.Net.Rest/Entities/Users/UserHelper.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 33b311604..1c5e5482c 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -34,7 +34,7 @@ namespace Discord /// Should the user have a nickname set? /// /// - /// To clear the user's nickname, this value can be set to null. + /// To clear the user's nickname, this value can be set to or . /// public Optional Nickname { get; set; } /// diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 562cfaae8..dfb81ff2c 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -48,6 +48,14 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) apiArgs.RoleIds = args.RoleIds.Value.ToArray(); + /* + * Ensure that the nick passed in the params of the request is not null. + * string.Empty ("") is the only way to reset the user nick in the API, + * a value of null does not. This is a workaround. + */ + if (apiArgs.Nickname.IsSpecified && apiArgs.Nickname.Value == null) + apiArgs.Nickname = new Optional(string.Empty); + await client.ApiClient.ModifyGuildMemberAsync(user.GuildId, user.Id, apiArgs, options).ConfigureAwait(false); return args; } From c08f37bb0344bc48159c6cebcd1599b30d12bf7e Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 5 Jan 2018 20:49:59 -0500 Subject: [PATCH 12/31] Fix nullref exceptions from Rich Presences --- .../Extensions/EntityExtensions.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index b59702be2..c66163610 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -17,9 +17,9 @@ State = model.State.GetValueOrDefault(), SmallAsset = assets?[0], LargeAsset = assets?[1], - Party = model.Party.GetValueOrDefault()?.ToEntity(), - Secrets = model.Secrets.GetValueOrDefault()?.ToEntity(), - Timestamps = model.Timestamps.GetValueOrDefault()?.ToEntity() + Party = model.Party.IsSpecified ? model.Party.Value.ToEntity() : null, + Secrets = model.Secrets.IsSpecified ? model.Secrets.Value.ToEntity() : null, + Timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null }; } // Stream Game @@ -56,11 +56,9 @@ public static GameParty ToEntity(this API.GameParty model) { - // todo: proper fix for this - if (model == null) return null; // Discord will probably send bad data since they don't validate anything int current = 0, cap = 0; - if (model.Size.Length == 2) + if (model.Size?.Length == 2) { current = model.Size[0]; cap = model.Size[1]; From 39bddca5d13965ea59a7dbd1317b76b29fe2fff3 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 6 Jan 2018 10:04:07 +0800 Subject: [PATCH 13/31] Fix broken markdown in command docs (#920) I screwed up in ##826 and broke the `CommandAttribute` section. --- docs/guides/commands/commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 6781764c9..2b012af0e 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -93,9 +93,9 @@ If you would like a parameter to parse until the end of a Command, flag the parameter with the [RemainderAttribute]. This will allow a user to invoke a Command without wrapping a parameter in quotes. -Finally, flag your Command with the [CommandAttribute] (you must +Finally, flag your Command with the [CommandAttribute]. (you must specify a name for this Command, except for when it is part of a -Module Group - see below). +Module Group - see below) [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute [CommandAttribute]: xref:Discord.Commands.CommandAttribute @@ -340,4 +340,4 @@ and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader]. -[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ \ No newline at end of file +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ From 030422fa1ddada549cea65cd87dae537cb1aaf76 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:27:51 -0500 Subject: [PATCH 14/31] Add support for channel categories (#907) commit a85c5814a74e473e95fe172f0379cbc7f9f951d8 Author: Christopher F Date: Sat Jan 6 22:25:48 2018 -0500 Code cleanup commit 4b243fd3dd99152b4ebc7ee01d704bd8e57eeee1 Author: Christopher F Date: Sat Jan 6 22:08:28 2018 -0500 Add support for channel categories (#907) commit 41ed9106f2b05530acbf06b245c9aa618011d815 Author: mrspits4ever Date: Thu Dec 14 20:02:57 2017 +0100 removed mentioning support for RestCategoryChannel, added channels property to SocketCategoryChannel commit 71142c310847886dff80c49e9357dd0786d67a1b Merge: 4589d731 678a7238 Author: mrspits4ever Date: Wed Dec 13 21:17:53 2017 +0100 Merge branch 'dev' of https://github.com/RogueException/Discord.Net into feature/channel-categories commit 4589d73187871c98485ed25c6d223706927af7ec Author: mrspits4ever Date: Wed Dec 13 21:17:46 2017 +0100 adressed requested changes commit d59b038efa048b2279602e2015ddd2c185e58d63 Author: pegasy Date: Mon Sep 25 18:53:23 2017 +0200 Renamed classes / properties / methods to use CategoryChannel instead of ChannelCategory to be consistant with how text / voice channels are named. commit 5c4777dc8cc443108f2e7e4afae98824c9a32b1f Author: pegasy Date: Sun Sep 24 19:08:25 2017 +0200 removed Guild from class name for ChannelCategory Renamed all properties to use Category instead of Parent Throw exception on GetUsers / GetInvites etc for categories commit e18bd8c799d2327270021c05866cb2e97ad4671b Author: pegasy Date: Sun Sep 24 15:49:51 2017 +0200 Add support for channel categories (as its own channel type) --- .../Entities/Channels/RpcGuildChannel.cs | 7 +++ .../Channels/GuildChannelProperties.cs | 4 ++ .../Entities/Channels/ICategoryChannel.cs | 12 +++++ .../Entities/Channels/IGuildChannel.cs | 6 ++- .../Entities/Guilds/IGuild.cs | 3 ++ src/Discord.Net.Rest/API/Common/Channel.cs | 2 + .../API/Rest/ModifyGuildChannelParams.cs | 2 + .../Entities/Channels/ChannelHelper.cs | 31 ++++++----- .../Entities/Channels/ChannelType.cs | 3 +- .../Entities/Channels/RestCategoryChannel.cs | 43 +++++++++++++++ .../Entities/Channels/RestGuildChannel.cs | 27 ++++++---- .../Entities/Guilds/GuildHelper.cs | 9 ++++ .../Entities/Guilds/RestGuild.cs | 28 +++++++--- .../Entities/Channels/RpcCategoryChannel.cs | 36 +++++++++++++ .../Channels/SocketCategoryChannel.cs | 53 +++++++++++++++++++ .../Entities/Channels/SocketGuildChannel.cs | 9 ++++ .../Entities/Guilds/SocketGuild.cs | 15 ++++-- 17 files changed, 256 insertions(+), 34 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs create mode 100644 src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs index 401263555..576a0489c 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGuildChannel.cs @@ -10,6 +10,7 @@ namespace Discord.Rpc { public ulong GuildId { get; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } internal RpcGuildChannel(DiscordRpcClient discord, ulong id, ulong guildId) : base(discord, id) @@ -57,6 +58,12 @@ namespace Discord.Rpc public override string ToString() => Name; //IGuildChannel + public Task GetCategoryAsync() + { + //Always fails + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + IGuild IGuildChannel.Guild { get diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 0ea196a4a..2ac6c8d52 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -26,5 +26,9 @@ /// Move the channel to the following position. This is 0-based! /// public Optional Position { get; set; } + /// + /// Sets the category for this channel + /// + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs new file mode 100644 index 000000000..0f7f5aa62 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface ICategoryChannel : IGuildChannel + { + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index c7cf0b3c2..c9841cb15 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -9,6 +9,10 @@ namespace Discord /// Gets the position of this channel in the guild's channel list, relative to others of the same type. int Position { get; } + /// Gets the parentid (category) of this channel in the guild's channel list. + ulong? CategoryId { get; } + /// Gets the parent channel (category) of this channel. + Task GetCategoryAsync(); /// Gets the guild this channel is a member of. IGuild Guild { get; } /// Gets the id of the guild this channel is a member of. @@ -23,7 +27,7 @@ namespace Discord Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); /// Returns a collection of all invites to this channel. Task> GetInvitesAsync(RequestOptions options = null); - + /// Modifies this guild channel. Task ModifyAsync(Action func, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 0fdd92405..2f0599d76 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -84,6 +84,7 @@ namespace Discord Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); @@ -93,6 +94,8 @@ namespace Discord Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); + /// Creates a new channel category. + Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 608ddcf66..97c35a57b 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -23,6 +23,8 @@ namespace Discord.API public Optional Position { get; set; } [JsonProperty("permission_overwrites")] public Optional PermissionOverwrites { get; set; } + [JsonProperty("parent_id")] + public ulong? CategoryId { get; set; } //TextChannel [JsonProperty("topic")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index b4add2ac9..120eeb3a8 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -10,5 +10,7 @@ namespace Discord.API.Rest public Optional Name { get; set; } [JsonProperty("position")] public Optional Position { get; set; } + [JsonProperty("parent_id")] + public Optional CategoryId { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index fa870be17..ad5029785 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -14,13 +14,13 @@ namespace Discord.Rest internal static class ChannelHelper { //General - public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, + public static async Task DeleteAsync(IChannel channel, BaseDiscordClient client, RequestOptions options) - { + { await client.ApiClient.DeleteChannelAsync(channel.Id, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IGuildChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new GuildChannelProperties(); @@ -28,12 +28,13 @@ namespace Discord.Rest var apiArgs = new API.Rest.ModifyGuildChannelParams { Name = args.Name, - Position = args.Position + Position = args.Position, + CategoryId = args.CategoryId }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(ITextChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new TextChannelProperties(); @@ -42,13 +43,14 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, Topic = args.Topic, IsNsfw = args.IsNsfw }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, - Action func, + public static async Task ModifyAsync(IVoiceChannel channel, BaseDiscordClient client, + Action func, RequestOptions options) { var args = new VoiceChannelProperties(); @@ -58,6 +60,7 @@ namespace Discord.Rest Bitrate = args.Bitrate, Name = args.Name, Position = args.Position, + CategoryId = args.CategoryId, UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); @@ -87,7 +90,7 @@ namespace Discord.Rest } //Messages - public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, + public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, ulong id, RequestOptions options) { var guildId = (channel as IGuildChannel)?.GuildId; @@ -98,7 +101,7 @@ namespace Discord.Rest var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } - public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { if (dir == Direction.Around) @@ -124,7 +127,7 @@ namespace Discord.Rest foreach (var model in models) { var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); - builder.Add(RestMessage.Create(client, channel, author, model)); + builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); }, @@ -180,7 +183,7 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); - } + } public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) @@ -277,7 +280,7 @@ namespace Discord.Rest { await client.ApiClient.TriggerTypingIndicatorAsync(channel.Id, options).ConfigureAwait(false); } - public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, + public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) => new TypingNotifier(client, channel, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index f05f1598e..e9f069a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs @@ -5,6 +5,7 @@ Text = 0, DM = 1, Voice = 2, - Group = 3 + Group = 3, + Category = 4 } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs new file mode 100644 index 000000000..397e14e76 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestCategoryChannel : RestGuildChannel, ICategoryChannel + { + internal RestCategoryChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + } + internal new static RestCategoryChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestCategoryChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 1ce1c8368..026d03cc8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -16,7 +16,7 @@ namespace Discord.Rest internal IGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } - + public ulong? CategoryId { get; private set; } public ulong GuildId => Guild.Id; internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -32,6 +32,8 @@ namespace Discord.Rest return RestTextChannel.Create(discord, guild, model); case ChannelType.Voice: return RestVoiceChannel.Create(discord, guild, model); + case ChannelType.Category: + return RestCategoryChannel.Create(discord, guild, model); default: // TODO: Channel categories return new RestGuildChannel(discord, guild, model.Id); @@ -61,7 +63,14 @@ namespace Discord.Rest } public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); - + + public async Task GetCategoryAsync() + { + if (CategoryId.HasValue) + return (await Guild.GetChannelAsync(CategoryId.Value).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + public OverwritePermissions? GetPermissionOverwrite(IUser user) { for (int i = 0; i < _overwrites.Length; i++) @@ -139,20 +148,20 @@ namespace Discord.Rest => await GetInvitesAsync(options).ConfigureAwait(false); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - - OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) + + OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); - async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) + async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); - async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) + async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); - + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7e25b53f3..12fdb075d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -157,6 +157,15 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } + public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var args = new CreateGuildChannelParams(name, ChannelType.Category); + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestCategoryChannel.Create(client, guild, model); + } //Integrations public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 3b6a2bfa8..401a121e0 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -23,7 +23,7 @@ namespace Discord.Rest public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } - + public ulong? AFKChannelId { get; private set; } public ulong? EmbedChannelId { get; private set; } public ulong? SystemChannelId { get; private set; } @@ -114,7 +114,7 @@ namespace Discord.Rest Update(model); } public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) - { + { var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } @@ -155,7 +155,7 @@ namespace Discord.Rest public Task> GetChannelsAsync(RequestOptions options = null) => GuildHelper.GetChannelsAsync(this, Discord, options); public Task GetChannelAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetChannelAsync(this, Discord, id, options); + => GuildHelper.GetChannelAsync(this, Discord, id, options); public async Task GetTextChannelAsync(ulong id, RequestOptions options = null) { var channel = await GuildHelper.GetChannelAsync(this, Discord, id, options).ConfigureAwait(false); @@ -176,6 +176,11 @@ namespace Discord.Rest var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); return channels.Select(x => x as RestVoiceChannel).Where(x => x != null).ToImmutableArray(); } + public async Task> GetCategoryChannelsAsync(RequestOptions options = null) + { + var channels = await GuildHelper.GetChannelsAsync(this, Discord, options).ConfigureAwait(false); + return channels.Select(x => x as RestCategoryChannel).Where(x => x != null).ToImmutableArray(); + } public async Task GetAFKChannelAsync(RequestOptions options = null) { @@ -199,7 +204,7 @@ namespace Discord.Rest public async Task GetEmbedChannelAsync(RequestOptions options = null) { var embedId = EmbedChannelId; - if (embedId.HasValue) + if (embedId.HasValue) return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } @@ -217,6 +222,8 @@ namespace Discord.Rest => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); //Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) @@ -236,7 +243,7 @@ namespace Discord.Rest return null; } - public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) { var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); @@ -320,6 +327,13 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetCategoryChannelsAsync(options).ConfigureAwait(false); + else + return null; + } async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -359,6 +373,8 @@ namespace Discord.Rest => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); @@ -368,7 +384,7 @@ namespace Discord.Rest async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); - IRole IGuild.GetRole(ulong id) + IRole IGuild.GetRole(ulong id) => GetRole(id); async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs new file mode 100644 index 000000000..cac766f92 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcCategoryChannel.cs @@ -0,0 +1,36 @@ +using Discord.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.Rpc.Channel; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcCategoryChannel : RpcGuildChannel, ICategoryChannel + { + public IReadOnlyCollection CachedMessages { get; private set; } + + public string Mention => MentionUtils.MentionChannel(Id); + + internal RpcCategoryChannel(DiscordRpcClient discord, ulong id, ulong guildId) + : base(discord, id, guildId) + { + } + internal new static RpcCategoryChannel Create(DiscordRpcClient discord, Model model) + { + var entity = new RpcCategoryChannel(discord, model.Id, model.GuildId.Value); + entity.Update(model); + return entity; + } + internal override void Update(Model model) + { + base.Update(model); + CachedMessages = model.Messages.Select(x => RpcMessage.Create(Discord, Id, x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs new file mode 100644 index 000000000..d5a183b1e --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord.Audio; +using Discord.Rest; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel + { + public override IReadOnlyCollection Users + => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + + public IReadOnlyCollection Channels + => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + + internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id, guild) + { + } + internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; + internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; + + // IGuildChannel + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException(); + Task> IGuildChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException(); + + //IChannel + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => throw new NotSupportedException(); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 8e24a5196..2163daf55 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -17,6 +17,9 @@ namespace Discord.WebSocket public SocketGuild Guild { get; } public string Name { get; private set; } public int Position { get; private set; } + public ulong? CategoryId { get; private set; } + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; public IReadOnlyCollection PermissionOverwrites => _overwrites; public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); @@ -34,6 +37,8 @@ namespace Discord.WebSocket return SocketTextChannel.Create(guild, state, model); case ChannelType.Voice: return SocketVoiceChannel.Create(guild, state, model); + case ChannelType.Category: + return SocketCategoryChannel.Create(guild, state, model); default: // TODO: Proper implementation for channel categories return new SocketGuildChannel(guild.Discord, model.Id, guild); @@ -43,6 +48,7 @@ namespace Discord.WebSocket { Name = model.Name.Value; Position = model.Position.Value; + CategoryId = model.CategoryId; var overwrites = model.PermissionOverwrites.Value; var newOverwrites = ImmutableArray.CreateBuilder(overwrites.Length); @@ -129,6 +135,9 @@ namespace Discord.WebSocket IGuild IGuildChannel.Guild => Guild; ulong IGuildChannel.GuildId => Guild.Id; + Task IGuildChannel.GetCategoryAsync() + => Task.FromResult(Category); + async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c4158c136..ea68a8f54 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -75,7 +75,7 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketGuildChannel EmbedChannel + public SocketGuildChannel EmbedChannel { get { @@ -95,6 +95,8 @@ namespace Discord.WebSocket => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); + public IReadOnlyCollection CategoryChannels + => Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray(); public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels @@ -317,6 +319,9 @@ namespace Discord.WebSocket => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); @@ -348,7 +353,7 @@ namespace Discord.WebSocket return value; return null; } - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); internal SocketRole AddRole(RoleModel model) @@ -594,7 +599,7 @@ namespace Discord.WebSocket try { await RepopulateAudioStreamsAsync().ConfigureAwait(false); - await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -651,6 +656,8 @@ namespace Discord.WebSocket => Task.FromResult(GetTextChannel(id)); Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); + Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) + => Task.FromResult>(CategoryChannels); Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) @@ -665,6 +672,8 @@ namespace Discord.WebSocket => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) + => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); From 5c8c78498b0f26358cbad5271f55bd2ab58dd7f0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:30:04 -0500 Subject: [PATCH 15/31] Add EditorConfig (#876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c692306fcc1e86ab92dd10683d3719f16c02a249 Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 10:49:00 2017 +0900 Add target of the internal fields commit 2d08f9a655b4949c1177f778d0f499047484a537 Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 10:17:42 2017 +0900 Add some more extension commit 4f19b835ffe8c64a93a9b4659e60b03ac797760f Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 01:19:11 2017 +0900 Add naming rules commit af756cd9feb630baadbf6025cbb079cd9e1f45cb Author: Acid Chicken (硫酸鶏) Date: Sat Nov 11 00:35:30 2017 +0900 Add basic .NET style rules commit 503ece558b4f07bd8008157d3aeb6a4e7100d349 Author: Acid Chicken (硫酸鶏) Date: Fri Nov 10 22:36:52 2017 +0900 Add EditorConfig --- .editorconfig | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..696bb866b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,113 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true + +[*.{csproj,json,md,nuspec,yml}] +indent_size = 2 +indent_style = space + +[*.{sln,xml}] +indent_style = tab + +[*.cs] +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_operators = true:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +csharp_prefer_simple_default_expression = false:none + +csharp_style_throw_expression = true:none +csharp_style_conditional_delegate_call = true:none +csharp_prefer_braces = false:none + +dotnet_sort_system_directives_first = false + +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_within_query_expression_clauses = true + +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false + +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +dotnet_naming_rule.all_of_const_fields_are_pascal_case.symbols = const_fields +dotnet_naming_rule.all_of_const_fields_are_pascal_case.style = pascal_case +dotnet_naming_rule.all_of_const_fields_are_pascal_case.severity = suggestion + +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.symbols = local_fields +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.style = starts_with_low_line_camel_case +dotnet_naming_rule.all_of_local_fields_without_const_are_camel_case.severity = suggestion + +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.symbols = interfaces +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.style = starts_with_i_pascal_case +dotnet_naming_rule.all_of_interfaces_starts_with_low_line_camel_case.severity = suggestion + +dotnet_naming_rule.default_is_pascal_case.symbols = without_interfaces_and_fields +dotnet_naming_rule.default_is_pascal_case.style = pascal_case +dotnet_naming_rule.default_is_pascal_case.severity = suggestion + +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.applicable_accessibilities = * +dotnet_naming_symbols.const_fields.required_modifiers = const + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * + +dotnet_naming_symbols.local_fields.applicable_kinds = field +dotnet_naming_symbols.local_fields.applicable_accessibilities = internal, private, protected, protected_internal +dotnet_naming_symbols.local_fields.required_modifiers = abstract, must_inherit, readonly, static, shared + +dotnet_naming_symbols.without_interfaces_and_fields.applicable_kinds = class, struct, enum, property, method, event, namespace, delegate, type_parameter +dotnet_naming_symbols.without_interfaces_and_fields.applicable_accessibilities = * + +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.starts_with_i_pascal_case.required_prefix = I +dotnet_naming_style.starts_with_i_pascal_case.capitalization = pascal_case + +dotnet_naming_style.starts_with_low_line_camel_case.required_prefix = _ +dotnet_naming_style.starts_with_low_line_camel_case.capitalization = camel_case From cd82a0f70cb7c350c36e35f9a70c8c1f4aa6bb27 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:30:52 -0500 Subject: [PATCH 16/31] Added a contributing guide (#828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 779ec4677e8b3979f83bcc6a673062ff6caa8be6 Author: Christopher F Date: Wed Sep 27 21:14:54 2017 -0400 Added a contributing guide Happy Hacktoberfest 🎃 --- CONTRIBUTING.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8248291e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing + +Discord.Net is an open-source project, and we appreciate any and all +contributions made by our community. However, please conform to the +following guidelines when possible: + +## Development Cycle + +We prefer all changes to the library to be discussed beforehand, +either in a GitHub issue, or in a discussion in our Discord channel +with library regulars or other contributors. + +Issues that are tagged as "up for grabs" are free to be picked up by +any member of the community. + +### Pull Requests + +We prefer pull-requests that are descriptive of the changes being made +and highlight any potential benefits/drawbacks of the change, but these +types of write-ups are not required. See this [merge request](https://github.com/RogueException/Discord.Net/pull/793) +for an example of a well-written description. + +## Semantic Versioning + +This project follows [Semantic Versioning](http://semver.org/). When +writing changes to this project, it is recommended to write changes +that are SemVer compliant with the latest version of the library in +development. + +The working release should be the latest build off of the `dev` branch, +but can also be found on the [development board](https://github.com/RogueException/Discord.Net/projects/1). + +We follow the .NET Foundation's [Breaking Change Rules](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md) +when determining the SemVer compliance of a change. + +Obsoleting a method is considered a **minor** increment. + +## Coding Style + +We attempt to conform to the .NET Foundation's [Coding Style](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md) +where possible. + +As a general rule, follow the coding style already set in the file you +are editing, or look at a similar file if you are adding a new one. \ No newline at end of file From 804d9188e79df57fc2d298107f9f1260c83b1ea1 Mon Sep 17 00:00:00 2001 From: BinkanSalaryman Date: Sun, 7 Jan 2018 04:33:00 +0100 Subject: [PATCH 17/31] Fix flawed bulk message deletion (#872) * Fix flawed bulk message deletion https://github.com/RogueException/Discord.Net/issues/871, consider changing DeleteMessagesParams.MessageIds type to I(Readonly)List or IEnumerable to avoid unnecessary copying (batch.ToArray()) * Update code formatting --- .../Entities/Channels/ChannelHelper.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index ad5029785..585e0a354 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -188,21 +188,27 @@ namespace Discord.Rest public static async Task DeleteMessagesAsync(ITextChannel channel, BaseDiscordClient client, IEnumerable messageIds, RequestOptions options) { + const int BATCH_SIZE = 100; + var msgs = messageIds.ToArray(); - if (msgs.Length < 100) - { - var args = new DeleteMessagesParams(msgs); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); - } - else + int batches = msgs.Length / BATCH_SIZE; + for (int i = 0; i <= batches; i++) { - var batch = new ulong[100]; - for (int i = 0; i < (msgs.Length + 99) / 100; i++) + ArraySegment batch; + if (i < batches) { - Array.Copy(msgs, i * 100, batch, 0, Math.Min(msgs.Length - (100 * i), 100)); - var args = new DeleteMessagesParams(batch); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + batch = new ArraySegment(msgs, i * BATCH_SIZE, BATCH_SIZE); } + else + { + batch = new ArraySegment(msgs, i * BATCH_SIZE, msgs.Length - batches * BATCH_SIZE); + if (batch.Count == 0) + { + break; + } + } + var args = new DeleteMessagesParams(batch.ToArray()); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); } } From edfbd055bb3ee62c565370af3abb84ab7214d2d6 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sat, 6 Jan 2018 19:35:17 -0800 Subject: [PATCH 18/31] Fix #854 Added ViewChannel enum and property to channel permissions (#874) * Fix #854 Added ViewChannel enum and property to channel permissions * replaced usages of ChannelPermission#ReadMessages with ViewChannel * rename parameter of ChannelPermissions constructor * made OverwritePermissions#ReadMessages obsolete, use ViewChannel instead * Fix #854 Added ViewChannel enum and property to channel permissions replaced usages of ChannelPermission#ReadMessages with ViewChannel rename parameter of ChannelPermissions constructor made OverwritePermissions#ReadMessages obsolete, use ViewChannel instead * renamed readMessages parameter in ChannelPermissions constructor and Modify * fixed channel permission tests to use ChannelPermission enum instead of GuildPermission enum * replaced usages of readmessages in channel permission tests * resolve build warnings for permission tests --- .../Entities/Permissions/ChannelPermission.cs | 4 +- .../Permissions/ChannelPermissions.cs | 18 +++-- .../Permissions/OverwritePermissions.cs | 12 ++-- src/Discord.Net.Core/Utils/Permissions.cs | 2 +- .../Entities/Channels/ChannelHelper.cs | 4 +- .../Entities/Guilds/RestGuild.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 4 +- .../Tests.ChannelPermissions.cs | 65 ++++++++++--------- .../Tests.GuildPermissions.cs | 7 +- 9 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 3e438f43f..740b6c30b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -11,7 +11,9 @@ namespace Discord // Text AddReactions = 0x00_00_00_40, - ReadMessages = 0x00_00_04_00, + [Obsolete("Use ViewChannel instead.")] + ReadMessages = ViewChannel, + ViewChannel = 0x00_00_04_00, SendMessages = 0x00_00_08_00, SendTTSMessages = 0x00_00_10_00, ManageMessages = 0x00_00_20_00, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 4c11d0db0..1a8aad53c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -41,7 +41,11 @@ namespace Discord /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); /// If True, a user may join channels. - public bool ReadMessages => Permissions.GetValue(RawValue, ChannelPermission.ReadMessages); + [Obsolete("Use ViewChannel instead.")] + public bool ReadMessages => ViewChannel; + /// If True, a user may view channels. + public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); + /// 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. @@ -82,7 +86,7 @@ namespace Discord private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool? useExternalEmojis = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) @@ -92,7 +96,7 @@ namespace Discord Permissions.SetValue(ref value, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref value, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref value, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref value, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref value, viewChannel, ChannelPermission.ViewChannel); Permissions.SetValue(ref value, sendMessages, ChannelPermission.SendMessages); Permissions.SetValue(ref value, sendTTSMessages, ChannelPermission.SendTTSMessages); Permissions.SetValue(ref value, manageMessages, ChannelPermission.ManageMessages); @@ -116,11 +120,11 @@ namespace Discord /// Creates a new ChannelPermissions with the provided permissions. public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, - bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool viewChannel = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool manageRoles = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + : this(0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } @@ -128,11 +132,11 @@ namespace Discord /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? manageRoles = null, bool? manageWebhooks = null) - => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks); diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index c3e296e2c..108b67273 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -27,7 +28,10 @@ namespace Discord /// If Allowed, a user may add reactions. public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. - public PermValue ReadMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ReadMessages); + [Obsolete("Use ViewChannel instead.")] + public PermValue ReadMessages => ViewChannel; + /// If Allowed, a user may join channels. + public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); /// If Allowed, a user may send messages. public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); /// If Allowed, a user may send text-to-speech messages. @@ -72,7 +76,7 @@ namespace Discord private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, PermValue? addReactions = null, - PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, + PermValue? viewChannel = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, PermValue? mentionEveryone = null, PermValue? useExternalEmojis = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, @@ -81,7 +85,7 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); Permissions.SetValue(ref allowValue, ref denyValue, addReactions, ChannelPermission.AddReactions); - Permissions.SetValue(ref allowValue, ref denyValue, readMessages, ChannelPermission.ReadMessages); + Permissions.SetValue(ref allowValue, ref denyValue, viewChannel, ChannelPermission.ViewChannel); 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); diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a7de90623..367926dd1 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -152,7 +152,7 @@ namespace Discord if (channel is ITextChannel textChannel) { - if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + if (!GetValue(resolvedPermissions, ChannelPermission.ViewChannel)) { //No read permission on a text channel removes all other permissions resolvedPermissions = 0; diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 585e0a354..f4b6c7f23 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -244,7 +244,7 @@ namespace Discord.Rest if (model == null) return null; var user = RestGuildUser.Create(client, guild, model); - if (!user.GetPermissions(channel).ReadMessages) + if (!user.GetPermissions(channel).ViewChannel) return null; return user; @@ -265,7 +265,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); return models .Select(x => RestGuildUser.Create(client, guild, x)) - .Where(x => x.GetPermissions(channel).ReadMessages) + .Where(x => x.GetPermissions(channel).ViewChannel) .ToImmutableArray(); }, nextPage: (info, lastPage) => diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 401a121e0..76ddc07ca 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -197,7 +197,7 @@ namespace Discord.Rest var channels = await GetTextChannelsAsync(options).ConfigureAwait(false); var user = await GetCurrentUserAsync(options).ConfigureAwait(false); return channels - .Where(c => user.GetPermissions(c).ReadMessages) + .Where(c => user.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index b6a304b50..7b8f572d2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -25,7 +25,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), - ChannelPermission.ReadMessages)).ToImmutableArray(); + ChannelPermission.ViewChannel)).ToImmutableArray(); internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -107,7 +107,7 @@ namespace Discord.WebSocket { var guildPerms = Permissions.ResolveGuild(Guild, user); var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); - if (Permissions.GetValue(channelPerms, ChannelPermission.ReadMessages)) + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) return user; } return null; diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs index 92234e88b..ac8ede4e4 100644 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public void TestChannelPermission() + public Task TestChannelPermission() { var perm = new ChannelPermissions(); @@ -29,7 +29,7 @@ namespace Discord ulong textChannel = (ulong)( ChannelPermission.CreateInstantInvite | ChannelPermission.ManageChannels | ChannelPermission.AddReactions - | ChannelPermission.ReadMessages + | ChannelPermission.ViewChannel | ChannelPermission.SendMessages | ChannelPermission.SendTTSMessages | ChannelPermission.ManageMessages @@ -59,7 +59,7 @@ namespace Discord // DM Channels ulong dmChannel = (ulong)( - ChannelPermission.ReadMessages + ChannelPermission.ViewChannel | ChannelPermission.SendMessages | ChannelPermission.EmbedLinks | ChannelPermission.AttachFiles @@ -82,9 +82,10 @@ namespace Discord | ChannelPermission.UseVAD ); Assert.Equal(groupChannel, ChannelPermissions.Group.RawValue); + return Task.CompletedTask; } - - public void TestChannelPermissionModify() + + public Task TestChannelPermissionModify() { // test channel permission modify @@ -96,7 +97,7 @@ namespace Discord // ensure that when modified it works perm = perm.Modify(createInstantInvite: true); Assert.True(perm.CreateInstantInvite); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.CreateInstantInvite); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.CreateInstantInvite); // set false again, move on to next permission perm = perm.Modify(createInstantInvite: false); @@ -108,7 +109,7 @@ namespace Discord perm = perm.Modify(manageChannel: true); Assert.True(perm.ManageChannel); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageChannels); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageChannels); perm = perm.Modify(manageChannel: false); Assert.False(perm.ManageChannel); @@ -119,21 +120,21 @@ namespace Discord perm = perm.Modify(addReactions: true); Assert.True(perm.AddReactions); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.AddReactions); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AddReactions); perm = perm.Modify(addReactions: false); Assert.False(perm.AddReactions); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); // individual permission test - Assert.False(perm.ReadMessages); + Assert.False(perm.ViewChannel); - perm = perm.Modify(readMessages: true); - Assert.True(perm.ReadMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessages); + perm = perm.Modify(viewChannel: true); + Assert.True(perm.ViewChannel); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ViewChannel); - perm = perm.Modify(readMessages: false); - Assert.False(perm.ReadMessages); + perm = perm.Modify(viewChannel: false); + Assert.False(perm.ViewChannel); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); // individual permission test @@ -141,7 +142,7 @@ namespace Discord perm = perm.Modify(sendMessages: true); Assert.True(perm.SendMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendMessages); perm = perm.Modify(sendMessages: false); Assert.False(perm.SendMessages); @@ -152,7 +153,7 @@ namespace Discord perm = perm.Modify(sendTTSMessages: true); Assert.True(perm.SendTTSMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.SendTTSMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.SendTTSMessages); perm = perm.Modify(sendTTSMessages: false); Assert.False(perm.SendTTSMessages); @@ -163,7 +164,7 @@ namespace Discord perm = perm.Modify(manageMessages: true); Assert.True(perm.ManageMessages); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageMessages); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageMessages); perm = perm.Modify(manageMessages: false); Assert.False(perm.ManageMessages); @@ -174,7 +175,7 @@ namespace Discord perm = perm.Modify(embedLinks: true); Assert.True(perm.EmbedLinks); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.EmbedLinks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.EmbedLinks); perm = perm.Modify(embedLinks: false); Assert.False(perm.EmbedLinks); @@ -185,7 +186,7 @@ namespace Discord perm = perm.Modify(attachFiles: true); Assert.True(perm.AttachFiles); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.AttachFiles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.AttachFiles); perm = perm.Modify(attachFiles: false); Assert.False(perm.AttachFiles); @@ -196,7 +197,7 @@ namespace Discord perm = perm.Modify(readMessageHistory: true); Assert.True(perm.ReadMessageHistory); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ReadMessageHistory); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ReadMessageHistory); perm = perm.Modify(readMessageHistory: false); Assert.False(perm.ReadMessageHistory); @@ -207,7 +208,7 @@ namespace Discord perm = perm.Modify(mentionEveryone: true); Assert.True(perm.MentionEveryone); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MentionEveryone); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MentionEveryone); perm = perm.Modify(mentionEveryone: false); Assert.False(perm.MentionEveryone); @@ -218,7 +219,7 @@ namespace Discord perm = perm.Modify(useExternalEmojis: true); Assert.True(perm.UseExternalEmojis); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseExternalEmojis); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseExternalEmojis); perm = perm.Modify(useExternalEmojis: false); Assert.False(perm.UseExternalEmojis); @@ -229,7 +230,7 @@ namespace Discord perm = perm.Modify(connect: true); Assert.True(perm.Connect); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.Connect); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Connect); perm = perm.Modify(connect: false); Assert.False(perm.Connect); @@ -240,7 +241,7 @@ namespace Discord perm = perm.Modify(speak: true); Assert.True(perm.Speak); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.Speak); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.Speak); perm = perm.Modify(speak: false); Assert.False(perm.Speak); @@ -251,7 +252,7 @@ namespace Discord perm = perm.Modify(muteMembers: true); Assert.True(perm.MuteMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MuteMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MuteMembers); perm = perm.Modify(muteMembers: false); Assert.False(perm.MuteMembers); @@ -262,7 +263,7 @@ namespace Discord perm = perm.Modify(deafenMembers: true); Assert.True(perm.DeafenMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.DeafenMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.DeafenMembers); perm = perm.Modify(deafenMembers: false); Assert.False(perm.DeafenMembers); @@ -273,7 +274,7 @@ namespace Discord perm = perm.Modify(moveMembers: true); Assert.True(perm.MoveMembers); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.MoveMembers); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.MoveMembers); perm = perm.Modify(moveMembers: false); Assert.False(perm.MoveMembers); @@ -284,7 +285,7 @@ namespace Discord perm = perm.Modify(useVoiceActivation: true); Assert.True(perm.UseVAD); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.UseVAD); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.UseVAD); perm = perm.Modify(useVoiceActivation: false); Assert.False(perm.UseVAD); @@ -295,7 +296,7 @@ namespace Discord perm = perm.Modify(manageRoles: true); Assert.True(perm.ManageRoles); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageRoles); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageRoles); perm = perm.Modify(manageRoles: false); Assert.False(perm.ManageRoles); @@ -306,19 +307,21 @@ namespace Discord perm = perm.Modify(manageWebhooks: true); Assert.True(perm.ManageWebhooks); - Assert.Equal(perm.RawValue, (ulong)GuildPermission.ManageWebhooks); + Assert.Equal(perm.RawValue, (ulong)ChannelPermission.ManageWebhooks); perm = perm.Modify(manageWebhooks: false); Assert.False(perm.ManageWebhooks); Assert.Equal(ChannelPermissions.None.RawValue, perm.RawValue); + return Task.CompletedTask; } [Fact] - public void TestChannelTypeResolution() + public Task TestChannelTypeResolution() { ITextChannel someChannel = null; // null channels will throw exception Assert.Throws(() => ChannelPermissions.All(someChannel)); + return Task.CompletedTask; } } } diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs index dc51600cf..bb113d221 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -7,7 +7,7 @@ namespace Discord public partial class Tests { [Fact] - public void TestGuildPermission() + public Task TestGuildPermission() { // Test Guild Permission Constructors var perm = new GuildPermissions(); @@ -46,10 +46,12 @@ namespace Discord GuildPermission.SendMessages | GuildPermission.SendTTSMessages | GuildPermission.EmbedLinks | GuildPermission.AttachFiles); Assert.Equal(webHookPermissions, GuildPermissions.Webhook.RawValue); + + return Task.CompletedTask; } [Fact] - public void TestGuildPermissionModify() + public Task TestGuildPermissionModify() { var perm = new GuildPermissions(); @@ -298,6 +300,7 @@ namespace Discord Assert.False(perm.ManageEmojis); Assert.Equal(GuildPermissions.None.RawValue, perm.RawValue); + return Task.CompletedTask; } } From 5bbd9bba8254e3b4ba21cf359955fd091a94d8a6 Mon Sep 17 00:00:00 2001 From: ObsidianMinor Date: Sat, 6 Jan 2018 21:43:11 -0600 Subject: [PATCH 19/31] Renamed existing Flatten method to FlattenAsync and added new Flatten method. Also fixed ClientHelper using incorrect guild batch count. (#744) --- .../Readers/UserTypeReader.cs | 14 ++--- .../Extensions/AsyncEnumerableExtensions.cs | 54 ++++++++++++++++++- src/Discord.Net.Rest/ClientHelper.cs | 4 +- .../Entities/Guilds/RestGuild.cs | 2 +- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index ca337aaf6..8fc330d4c 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -13,7 +13,7 @@ namespace Discord.Commands public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); - IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? + IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better IReadOnlyCollection guildUsers = ImmutableArray.Create(); ulong id; @@ -45,7 +45,7 @@ namespace Discord.Commands string username = input.Substring(0, index); if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { - var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); @@ -57,8 +57,9 @@ namespace Discord.Commands //By Username (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); @@ -66,8 +67,9 @@ namespace Discord.Commands //By Nickname (0.5-0.6) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))) - AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); + await channelUsers + .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) + .ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f)); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, (guildUser as IGuildUser).Nickname == input ? 0.60f : 0.50f); diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index f52edd719..345154f1d 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -1,14 +1,64 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Discord { public static class AsyncEnumerableExtensions { - public static async Task> Flatten(this IAsyncEnumerable> source) + /// + /// Flattens the specified pages into one asynchronously + /// + /// + /// + /// + public static async Task> FlattenAsync(this IAsyncEnumerable> source) { - return (await source.ToArray().ConfigureAwait(false)).SelectMany(x => x); + return await source.Flatten().ToArray().ConfigureAwait(false); + } + + public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) + { + return new PagedCollectionEnumerator(source); + } + + internal class PagedCollectionEnumerator : IAsyncEnumerator, IAsyncEnumerable + { + readonly IAsyncEnumerator> _source; + IEnumerator _enumerator; + + public IAsyncEnumerator GetEnumerator() => this; + + internal PagedCollectionEnumerator(IAsyncEnumerable> source) + { + _source = source.GetEnumerator(); + } + + public T Current => _enumerator.Current; + + public void Dispose() + { + _enumerator?.Dispose(); + _source.Dispose(); + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if(!_enumerator?.MoveNext() ?? true) + { + if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) + return false; + + _enumerator?.Dispose(); + _enumerator = _source.Current.GetEnumerator(); + return _enumerator.MoveNext(); + } + + return true; + } } } } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 26d8c720e..5c9e26433 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -79,7 +79,7 @@ namespace Discord.Rest ulong? fromGuildId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxUsersPerBatch, + DiscordConfig.MaxGuildsPerBatch, async (info, ct) => { var args = new GetGuildSummariesParams @@ -106,7 +106,7 @@ namespace Discord.Rest } public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) { - var summaryModels = await GetGuildSummariesAsync(client, null, null, options).Flatten(); + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).FlattenAsync().ConfigureAwait(false); var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 76ddc07ca..5d12731a6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -413,7 +413,7 @@ namespace Discord.Rest async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return (await GetUsersAsync(options).Flatten().ConfigureAwait(false)).ToImmutableArray(); + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } From d5e9d6f9c1e70c7b6c569615e491de1149324f56 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 22:54:50 -0500 Subject: [PATCH 20/31] Fix ShardedClient#GetShardFor null-case (#742) This resolves #742. Common cases for IGuild being null on access are DMs (since they do not belong to a guild) - this change resolves null guilds to shard zero, where DMs are also received. --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index e827909d9..4e99ae28d 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -133,7 +133,7 @@ namespace Discord.WebSocket private DiscordSocketClient GetShardFor(ulong guildId) => GetShard(GetShardIdFor(guildId)); public DiscordSocketClient GetShardFor(IGuild guild) - => GetShardFor(guild.Id); + => GetShardFor(guild?.Id ?? 0); /// public override async Task GetApplicationInfoAsync(RequestOptions options = null) From fdd2c80d2b47c4dcb507ed54352f592f890b9f93 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 23:20:21 -0500 Subject: [PATCH 21/31] Fix outgoing activity sending (#916) This change resolves #916 Discord requires the {"type": 0} payload for all non-streaming activities. This change fixes a bug where name-only games would fail to include this payload, causing the presence change to be discarded by Discord. --- .../DiscordSocketClient.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 35a22edc4..cb3f23c57 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -328,9 +328,9 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (streamUrl != null) + if (!string.IsNullOrEmpty(streamUrl)) Activity = new StreamingGame(name, streamUrl, streamType); - else if (name != null) + else if (!string.IsNullOrEmpty(name)) Activity = new Game(name); else Activity = null; @@ -346,21 +346,24 @@ namespace Discord.WebSocket { if (CurrentUser == null) return; - var activity = Activity; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, activity); + CurrentUser.Presence = new SocketPresence(status, Activity); var gameModel = new GameModel(); // Discord only accepts rich presence over RPC, don't even bother building a payload - if (activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); - if (activity is StreamingGame stream) + if (Activity is RichGame game) + throw new NotSupportedException("Outgoing Rich Presences are not supported"); + else if (Activity is StreamingGame stream) { gameModel.StreamUrl = stream.Url; gameModel.StreamType = stream.StreamType; } - else if (activity != null) - gameModel.Name = activity.Name; + else if (Activity != null) + { + gameModel.Name = Activity.Name; + gameModel.StreamType = StreamType.NotStreaming; + } await ApiClient.SendStatusUpdateAsync( status, From 9d77a3cd3757a48a3ca77aa50071016209e40f02 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 6 Jan 2018 23:36:08 -0500 Subject: [PATCH 22/31] Fix parsing timestamps with a timezone attached Resolves #918. For some reason (that i'm sure will bite me in the ass later), we configured Json.Net with a time format that forced UTC - even when the API appends a timezone to the timestamp. Removing the custom time format seems to resolve this issue. --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ab47b1e98..689cba9c3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 #pragma warning disable CS0618 using Discord.API.Rest; using Discord.Net; @@ -52,7 +52,7 @@ namespace Discord.API _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; - _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; + _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); From 0f2af47ec842da517b05b2fb9aefbc62a5674466 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 7 Jan 2018 15:55:01 -0500 Subject: [PATCH 23/31] Fix typo in API model for GameAssets This would cause the large asset's Image ID (and in turn, Image URL) and hover text to be reversed. --- src/Discord.Net.Rest/API/Common/GameAssets.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs index b5928a8ab..94a540769 100644 --- a/src/Discord.Net.Rest/API/Common/GameAssets.cs +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { @@ -8,9 +8,9 @@ namespace Discord.API public Optional SmallText { get; set; } [JsonProperty("small_image")] public Optional SmallImage { get; set; } - [JsonProperty("large_image")] - public Optional LargeText { get; set; } [JsonProperty("large_text")] + public Optional LargeText { get; set; } + [JsonProperty("large_image")] public Optional LargeImage { get; set; } } -} \ No newline at end of file +} From 42c879c37cdac8ec8358f80f08105f2a21255b31 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 8 Jan 2018 02:29:24 -0500 Subject: [PATCH 24/31] Add 'html' to EmbedType enum This resolves #762. This change adds an 'html' variant to the EmbedType enum. This change also adds an 'Unknown' variant to the EmbedType enum (at position -1); this will be used in a later commit to future-proof the EmbedType enum from any further variants Discord may add. --- src/Discord.Net.Core/Entities/Messages/EmbedType.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index 469e968a5..5bb2653e2 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -1,13 +1,15 @@ -namespace Discord +namespace Discord { public enum EmbedType { + Unknown = -1, Rich, Link, Video, Image, Gifv, Article, - Tweet + Tweet, + Html, } } From 97397f36177ab03835f31f3e051160411ee6df7c Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 8 Jan 2018 12:56:31 -0500 Subject: [PATCH 25/31] Apply consistency to attributes in the commands extension (#928) * Apply consistency to attributes in the commands extension This resolves #527. Not sure if I missed any, putting this up for review. * Allow preconditions to be used multiple times (for use with groups) --- src/Discord.Net.Commands/Attributes/AliasAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/CommandAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/GroupAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/NameAttribute.cs | 2 +- .../Attributes/OverrideTypeReaderAttribute.cs | 4 ++-- .../Attributes/ParameterPreconditionAttribute.cs | 3 +-- .../Attributes/Preconditions/RequireContextAttribute.cs | 4 ++-- .../Attributes/Preconditions/RequireNsfwAttribute.cs | 4 ++-- .../Attributes/Preconditions/RequireOwnerAttribute.cs | 5 ++--- src/Discord.Net.Commands/Attributes/PriorityAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/RemainderAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/RemarksAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/SummaryAttribute.cs | 4 ++-- 15 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 6e115bd60..6cd0abbb7 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Commands { /// Provides aliases for a command. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class AliasAttribute : Attribute { /// The aliases which have been defined for the command. diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 5ae6092eb..5f8e9ceaf 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index d6a1c646e..cc23a6d15 100644 --- a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class DontAutoLoadAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs index bd966e129..c982d93a1 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class DontInjectAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index 105d256ec..b1760d149 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class GroupAttribute : Attribute { public string Prefix { get; } diff --git a/src/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/Discord.Net.Commands/Attributes/NameAttribute.cs index 0a5156fee..4a4b2bfed 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Commands { // Override public name of command/module - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class NameAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 37f685c95..44ab6d214 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -4,7 +4,7 @@ using System.Reflection; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); @@ -19,4 +19,4 @@ namespace Discord.Commands TypeReader = overridenTypeReader; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 209822583..3c5e8cf92 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -9,4 +8,4 @@ namespace Discord.Commands { public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 5fa0fb1b9..90af035e4 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -15,7 +15,7 @@ namespace Discord.Commands /// /// Require that the command be invoked in a specified context. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { public ContextType Contexts { get; } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index c8e3bfa82..273c764bd 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -6,7 +6,7 @@ namespace Discord.Commands /// /// Require that the command is invoked in a channel marked NSFW /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index e370aeec4..7a8a009be 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,7 +1,6 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -9,7 +8,7 @@ namespace Discord.Commands /// Require that the command is invoked by the owner of the bot. /// /// This precondition will only work if the bot is a bot account. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs index 5120bb7d1..353e96e41 100644 --- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Commands { /// Sets priority of commands - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class PriorityAttribute : Attribute { /// The priority which has been set for the command diff --git a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs index 4aa16bebb..56938f167 100644 --- a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class RemainderAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs index 44db18a79..c11f790a7 100644 --- a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace Discord.Commands { // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class RemarksAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs index 46d52f3d9..641163408 100644 --- a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace Discord.Commands { // Cosmetic Summary, for Groups and Commands - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class SummaryAttribute : Attribute { public string Text { get; } From b5e75486512491f5adcc10709c5417d3410c0113 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 11 Jan 2018 03:31:43 +0100 Subject: [PATCH 26/31] Comparers (#929) * Add entity equality comparers * Fix namespace #whoops * Add Message comparer. * Add comment explaining the specialized implementation * Remove specialized implementation, as per feedback --- src/Discord.Net.Core/Utils/Comparers.cs | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/Discord.Net.Core/Utils/Comparers.cs diff --git a/src/Discord.Net.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs new file mode 100644 index 000000000..d7641e897 --- /dev/null +++ b/src/Discord.Net.Core/Utils/Comparers.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + public static class DiscordComparers + { + // TODO: simplify with '??=' slated for C# 8.0 + public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); + public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); + public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); + public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); + public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); + + private static IEqualityComparer _userComparer; + private static IEqualityComparer _guildComparer; + private static IEqualityComparer _channelComparer; + private static IEqualityComparer _roleComparer; + private static IEqualityComparer _messageComparer; + + private sealed class EntityEqualityComparer : EqualityComparer + where TEntity : IEntity + where TId : IEquatable + { + public override bool Equals(TEntity x, TEntity y) + { + bool xNull = x == null; + bool yNull = y == null; + + if (xNull && yNull) + return true; + + if (xNull ^ yNull) + return false; + + return x.Id.Equals(y.Id); + } + + public override int GetHashCode(TEntity obj) + { + return obj?.Id.GetHashCode() ?? 0; + } + } + } +} From 87124d3e39b6aaf85ccb74724acf27d65f847cfe Mon Sep 17 00:00:00 2001 From: ObsidianMinor Date: Fri, 12 Jan 2018 16:24:01 -0600 Subject: [PATCH 27/31] Simplify Flatten extension (#933) --- .../Extensions/AsyncEnumerableExtensions.cs | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index 345154f1d..dd16d2943 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace Discord @@ -20,45 +19,7 @@ namespace Discord public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) { - return new PagedCollectionEnumerator(source); - } - - internal class PagedCollectionEnumerator : IAsyncEnumerator, IAsyncEnumerable - { - readonly IAsyncEnumerator> _source; - IEnumerator _enumerator; - - public IAsyncEnumerator GetEnumerator() => this; - - internal PagedCollectionEnumerator(IAsyncEnumerable> source) - { - _source = source.GetEnumerator(); - } - - public T Current => _enumerator.Current; - - public void Dispose() - { - _enumerator?.Dispose(); - _source.Dispose(); - } - - public async Task MoveNext(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(!_enumerator?.MoveNext() ?? true) - { - if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) - return false; - - _enumerator?.Dispose(); - _enumerator = _source.Current.GetEnumerator(); - return _enumerator.MoveNext(); - } - - return true; - } + return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); } } } From f69ef2a8cad3654c36684bee8f09eb246cce4e65 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sun, 14 Jan 2018 04:54:47 +0100 Subject: [PATCH 28/31] Add API Analyzer Assembly (#906) * Start on API analyzers * Finish GuildAccessAnalyzer * Update build script (will this do?) * Correct slashes * Extrapolate DerivesFromModuleBase() to an extension method * Quick refactoring * Add doc file --- Discord.Net.sln | 17 +- appveyor.yml | 1 + .../Discord.Net.Analyzers.csproj | 15 + .../GuildAccessAnalyzer.cs | 70 +++++ src/Discord.Net.Analyzers/SymbolExtensions.cs | 21 ++ src/Discord.Net.Analyzers/docs/DNET0001.md | 30 ++ .../Extensions/AppDomainPolyfill.cs | 30 ++ .../AnalyzerTests/GuildAccessTests.cs | 111 +++++++ .../Helpers/CodeFixVerifier.Helper.cs | 85 ++++++ .../AnalyzerTests/Helpers/DiagnosticResult.cs | 87 ++++++ .../Helpers/DiagnosticVerifier.Helper.cs | 207 +++++++++++++ .../Verifiers/CodeFixVerifier.cs | 129 +++++++++ .../Verifiers/DiagnosticVerifier.cs | 271 ++++++++++++++++++ .../Discord.Net.Tests.csproj | 1 + 14 files changed, 1074 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj create mode 100644 src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs create mode 100644 src/Discord.Net.Analyzers/SymbolExtensions.cs create mode 100644 src/Discord.Net.Analyzers/docs/DNET0001.md create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index cac6c9064..daf902b96 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 +VisualStudioVersion = 15.0.27130.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,18 @@ Global {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.Build.0 = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,6 +140,7 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/appveyor.yml b/appveyor.yml index 393485fee..54b9a1251 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,6 +29,7 @@ after_build: - ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: >- if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj new file mode 100644 index 000000000..8ab398ff5 --- /dev/null +++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -0,0 +1,15 @@ + + + + Discord.Net.Analyzers + Discord.Analyzers + A Discord.Net extension adding support for design-time analysis of the API usage. + netstandard1.3 + + + + + + + + diff --git a/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs new file mode 100644 index 000000000..0760d019f --- /dev/null +++ b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Discord.Commands; + +namespace Discord.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class GuildAccessAnalyzer : DiagnosticAnalyzer + { + private const string DiagnosticId = "DNET0001"; + private const string Title = "Limit command to Guild contexts."; + private const string MessageFormat = "Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts."; + private const string Description = "Accessing 'Context.Guild' in a command without limiting the command to run only in guilds."; + private const string Category = "API Usage"; + + private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + } + + private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + // Bail out if the accessed member isn't named 'Guild' + var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(context.Node).Symbol; + if (memberAccessSymbol.Name != "Guild") + return; + + // Bail out if it happens to be 'ContextType.Guild' in the '[RequireContext]' argument + if (context.Node.Parent is AttributeArgumentSyntax) + return; + + // Bail out if the containing class doesn't derive from 'ModuleBase' + var typeNode = context.Node.FirstAncestorOrSelf(); + var typeSymbol = context.SemanticModel.GetDeclaredSymbol(typeNode); + if (!typeSymbol.DerivesFromModuleBase()) + return; + + // Bail out if the containing method isn't marked with '[Command]' + var methodNode = context.Node.FirstAncestorOrSelf(); + var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode); + var methodAttributes = methodSymbol.GetAttributes(); + if (!methodAttributes.Any(a => a.AttributeClass.Name == nameof(CommandAttribute))) + return; + + // Is the '[RequireContext]' attribute not applied to either the + // method or the class, or its argument isn't 'ContextType.Guild'? + var ctxAttribute = methodAttributes.SingleOrDefault(_attributeDataPredicate) + ?? typeSymbol.GetAttributes().SingleOrDefault(_attributeDataPredicate); + + if (ctxAttribute == null || ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals((int)ContextType.Guild))) + { + // Report the diagnostic + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + } + + private static readonly Func _attributeDataPredicate = + (a => a.AttributeClass.Name == nameof(RequireContextAttribute)); + } +} diff --git a/src/Discord.Net.Analyzers/SymbolExtensions.cs b/src/Discord.Net.Analyzers/SymbolExtensions.cs new file mode 100644 index 000000000..680de66b5 --- /dev/null +++ b/src/Discord.Net.Analyzers/SymbolExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.CodeAnalysis; +using Discord.Commands; + +namespace Discord.Analyzers +{ + internal static class SymbolExtensions + { + private static readonly string _moduleBaseName = typeof(ModuleBase<>).Name; + + public static bool DerivesFromModuleBase(this ITypeSymbol symbol) + { + for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType) + { + if (bType.MetadataName == _moduleBaseName) + return true; + } + return false; + } + } +} diff --git a/src/Discord.Net.Analyzers/docs/DNET0001.md b/src/Discord.Net.Analyzers/docs/DNET0001.md new file mode 100644 index 000000000..0c1b8098f --- /dev/null +++ b/src/Discord.Net.Analyzers/docs/DNET0001.md @@ -0,0 +1,30 @@ +# DNET0001 + + + + + + + + + + + + + + +
TypeNameGuildAccessAnalyzer
CheckIdDNET0001
CategoryAPI Usage
+ +## Cause + +A method identified as a command is accessing `Context.Guild` without the requisite precondition. + +## Rule description + +The value of `Context.Guild` is `null` if a command is invoked in a DM channel. Attempting to access +guild properties in such a case will result in a `NullReferenceException` at runtime. +This exception is entirely avoidable by using the library's provided preconditions. + +## How to fix violations + +Add the precondition `[RequireContext(ContextType.Guild)]` to the command or module class. diff --git a/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs b/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs new file mode 100644 index 000000000..729bc385c --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Reflection; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Extensions.DependencyModel; + +namespace System +{ + /// Polyfill of the AppDomain class from full framework. + internal class AppDomain + { + public static AppDomain CurrentDomain { get; private set; } + + private AppDomain() + { + } + + static AppDomain() + { + CurrentDomain = new AppDomain(); + } + + public Assembly[] GetAssemblies() + { + var rid = RuntimeEnvironment.GetRuntimeIdentifier(); + var ass = DependencyContext.Default.GetRuntimeAssemblyNames(rid); + + return ass.Select(xan => Assembly.Load(xan)).ToArray(); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs b/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs new file mode 100644 index 000000000..073cc1de7 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Discord.Analyzers; +using TestHelper; +using Xunit; + +namespace Discord +{ + public partial class AnalyserTests + { + public class GuildAccessTests : DiagnosticVerifier + { + [Fact] + public void VerifyDiagnosticWhenLackingRequireContext() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test"")] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + var expected = new DiagnosticResult() + { + Id = "DNET0001", + Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) }, + Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.", + Severity = DiagnosticSeverity.Warning + }; + VerifyCSharpDiagnostic(source, expected); + } + + [Fact] + public void VerifyDiagnosticWhenWrongRequireContext() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test""), RequireContext(ContextType.Group)] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + var expected = new DiagnosticResult() + { + Id = "DNET0001", + Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) }, + Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.", + Severity = DiagnosticSeverity.Warning + }; + VerifyCSharpDiagnostic(source, expected); + } + + [Fact] + public void VerifyNoDiagnosticWhenRequireContextOnMethod() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test""), RequireContext(ContextType.Guild)] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + + VerifyCSharpDiagnostic(source, Array.Empty()); + } + + [Fact] + public void VerifyNoDiagnosticWhenRequireContextOnClass() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + [RequireContext(ContextType.Guild)] + public class TestModule : ModuleBase + { + [Command(""test"")] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + + VerifyCSharpDiagnostic(source, Array.Empty()); + } + + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + => new GuildAccessAnalyzer(); + } + } +} diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs new file mode 100644 index 000000000..0f73d0643 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs @@ -0,0 +1,85 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace TestHelper +{ + /// + /// Diagnostic Producer class with extra methods dealing with applying codefixes + /// All methods are static + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// + /// Apply the inputted CodeAction to the inputted document. + /// Meant to be used to apply codefixes. + /// + /// The Document to apply the fix on + /// A CodeAction that will be applied to the Document. + /// A Document with the changes from the CodeAction + private static Document ApplyFix(Document document, CodeAction codeAction) + { + var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; + var solution = operations.OfType().Single().ChangedSolution; + return solution.GetDocument(document.Id); + } + + /// + /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. + /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, + /// this method may not necessarily return the new one. + /// + /// The Diagnostics that existed in the code before the CodeFix was applied + /// The Diagnostics that exist in the code after the CodeFix was applied + /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied + private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) + { + var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + + int oldIndex = 0; + int newIndex = 0; + + while (newIndex < newArray.Length) + { + if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) + { + ++oldIndex; + ++newIndex; + } + else + { + yield return newArray[newIndex++]; + } + } + } + + /// + /// Get the existing compiler diagnostics on the inputted document. + /// + /// The Document to run the compiler diagnostic analyzers on + /// The compiler diagnostics that were found in the code + private static IEnumerable GetCompilerDiagnostics(Document document) + { + return document.GetSemanticModelAsync().Result.GetDiagnostics(); + } + + /// + /// Given a document, turn it into a string based on the syntax root + /// + /// The Document to be converted to a string + /// A string containing the syntax of the Document after formatting + private static string GetStringFromDocument(Document document) + { + var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; + var root = simplifiedDoc.GetSyntaxRootAsync().Result; + root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); + return root.GetText().ToString(); + } + } +} + diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs new file mode 100644 index 000000000..5ae6f528e --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis; +using System; + +namespace TestHelper +{ + /// + /// Location where the diagnostic appears, as determined by path, line number, and column number. + /// + public struct DiagnosticResultLocation + { + public DiagnosticResultLocation(string path, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); + } + + this.Path = path; + this.Line = line; + this.Column = column; + } + + public string Path { get; } + public int Line { get; } + public int Column { get; } + } + + /// + /// Struct that stores information about a Diagnostic appearing in a source + /// + public struct DiagnosticResult + { + private DiagnosticResultLocation[] locations; + + public DiagnosticResultLocation[] Locations + { + get + { + if (this.locations == null) + { + this.locations = new DiagnosticResultLocation[] { }; + } + return this.locations; + } + + set + { + this.locations = value; + } + } + + public DiagnosticSeverity Severity { get; set; } + + public string Id { get; set; } + + public string Message { get; set; } + + public string Path + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Path : ""; + } + } + + public int Line + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Line : -1; + } + } + + public int Column + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Column : -1; + } + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs new file mode 100644 index 000000000..7a8eb2e9c --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Discord; +using Discord.Commands; + +namespace TestHelper +{ + /// + /// Class for turning strings into documents and getting the diagnostics on them + /// All methods are static + /// + public abstract partial class DiagnosticVerifier + { + private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).GetTypeInfo().Assembly.Location); + //private static readonly MetadataReference DiscordNetReference = MetadataReference.CreateFromFile(typeof(IDiscordClient).GetTypeInfo().Assembly.Location); + //private static readonly MetadataReference DiscordCommandsReference = MetadataReference.CreateFromFile(typeof(CommandAttribute).GetTypeInfo().Assembly.Location); + private static readonly Assembly DiscordCommandsAssembly = typeof(CommandAttribute).GetTypeInfo().Assembly; + + internal static string DefaultFilePathPrefix = "Test"; + internal static string CSharpDefaultFileExt = "cs"; + internal static string VisualBasicDefaultExt = "vb"; + internal static string TestProjectName = "TestProject"; + + #region Get Diagnostics + + /// + /// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// + /// Classes in the form of strings + /// The language the source classes are in + /// The analyzer to be run on the sources + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) + { + return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); + } + + /// + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// + /// The analyzer to run on the documents + /// The Documents that the analyzer will be run on + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) + { + var projects = new HashSet(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = new List(); + foreach (var project in projects) + { + var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); + var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + for (int i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var tree = document.GetSyntaxTreeAsync().Result; + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + var results = SortDiagnostics(diagnostics); + diagnostics.Clear(); + return results; + } + + /// + /// Sort diagnostics by location in source document + /// + /// The list of Diagnostics to be sorted + /// An IEnumerable containing the Diagnostics in order of Location + private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) + { + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + #endregion + + #region Set up compilation and documents + /// + /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant + private static Document[] GetDocuments(string[] sources, string language) + { + if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) + { + throw new ArgumentException("Unsupported Language"); + } + + var project = CreateProject(sources, language); + var documents = project.Documents.ToArray(); + + if (sources.Length != documents.Length) + { + throw new Exception("Amount of sources did not match amount of Documents created"); + } + + return documents; + } + + /// + /// Create a Document from a string through creating a project that contains it. + /// + /// Classes in the form of a string + /// The language the source code is in + /// A Document created from the source string + protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) + { + return CreateProject(new[] { source }, language).Documents.First(); + } + + /// + /// Create a project using the inputted strings as sources. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Project created out of the Documents created from the source strings + private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) + { + string fileNamePrefix = DefaultFilePathPrefix; + string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, language) + .AddMetadataReference(projectId, CorlibReference) + .AddMetadataReference(projectId, SystemCoreReference) + .AddMetadataReference(projectId, CSharpSymbolsReference) + .AddMetadataReference(projectId, CodeAnalysisReference) + .AddMetadataReferences(projectId, Transitive(DiscordCommandsAssembly)); + + int count = 0; + foreach (var source in sources) + { + var newFileName = fileNamePrefix + count + "." + fileExt; + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + return solution.GetProject(projectId); + } + #endregion + + /// + /// Get the for and all assemblies referenced by + /// + /// The assembly. + /// s. + private static IEnumerable Transitive(Assembly assembly) + { + foreach (var a in RecursiveReferencedAssemblies(assembly)) + { + yield return MetadataReference.CreateFromFile(a.Location); + } + } + + private static HashSet RecursiveReferencedAssemblies(Assembly a, HashSet assemblies = null) + { + assemblies = assemblies ?? new HashSet(); + if (assemblies.Add(a)) + { + foreach (var referencedAssemblyName in a.GetReferencedAssemblies()) + { + var referencedAssembly = AppDomain.CurrentDomain.GetAssemblies() + .SingleOrDefault(x => x.GetName() == referencedAssemblyName) ?? + Assembly.Load(referencedAssemblyName); + RecursiveReferencedAssemblies(referencedAssembly, assemblies); + } + } + + return assemblies; + } + } +} + diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs new file mode 100644 index 000000000..5d057b610 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs @@ -0,0 +1,129 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Formatting; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Xunit; + +namespace TestHelper +{ + /// + /// Superclass of all Unit tests made for diagnostics with codefixes. + /// Contains methods used to verify correctness of codefixes + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// + /// Returns the codefix being tested (C#) - to be implemented in non-abstract class + /// + /// The CodeFixProvider to be used for CSharp code + protected virtual CodeFixProvider GetCSharpCodeFixProvider() + { + return null; + } + + /// + /// Returns the codefix being tested (VB) - to be implemented in non-abstract class + /// + /// The CodeFixProvider to be used for VisualBasic code + protected virtual CodeFixProvider GetBasicCodeFixProvider() + { + return null; + } + + /// + /// Called to test a C# codefix when applied on the inputted string as a source + /// + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// + /// Called to test a VB codefix when applied on the inputted string as a source + /// + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// + /// General verifier for codefixes. + /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. + /// Then gets the string after the codefix is applied and compares it with the expected result. + /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. + /// + /// The language the source code is in + /// The analyzer to be applied to the source code + /// The codefix to be applied to the code wherever the relevant Diagnostic is found + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) + { + var document = CreateDocument(oldSource, language); + var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + var compilerDiagnostics = GetCompilerDiagnostics(document); + var attempts = analyzerDiagnostics.Length; + + for (int i = 0; i < attempts; ++i) + { + var actions = new List(); + var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); + codeFixProvider.RegisterCodeFixesAsync(context).Wait(); + + if (!actions.Any()) + { + break; + } + + if (codeFixIndex != null) + { + document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); + break; + } + + document = ApplyFix(document, actions.ElementAt(0)); + analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + + var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + //check if applying the code fix introduced any new compiler diagnostics + if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) + { + // Format and get the compiler diagnostics again so that the locations make sense in the output + document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); + newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + Assert.True(false, + string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", + string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), + document.GetSyntaxRootAsync().Result.ToFullString())); + } + + //check if there are analyzer diagnostics left after the code fix + if (!analyzerDiagnostics.Any()) + { + break; + } + } + + //after applying all of the code fixes, compare the resulting string to the inputted one + var actual = GetStringFromDocument(document); + Assert.Equal(newSource, actual); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs new file mode 100644 index 000000000..498e5ef27 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs @@ -0,0 +1,271 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; + +namespace TestHelper +{ + /// + /// Superclass of all Unit Tests for DiagnosticAnalyzers + /// + public abstract partial class DiagnosticVerifier + { + #region To be implemented by Test classes + /// + /// Get the CSharp analyzer being tested - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return null; + } + + /// + /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() + { + return null; + } + #endregion + + #region Verifier wrappers + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// A class in the form of a string to run the analyzer on + /// DiagnosticResults that should appear after the analyzer is run on the source + protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// A class in the form of a string to run the analyzer on + /// DiagnosticResults that should appear after the analyzer is run on the source + protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// An array of strings to create source documents from to run the analyzers on + /// DiagnosticResults that should appear after the analyzer is run on the sources + protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// An array of strings to create source documents from to run the analyzers on + /// DiagnosticResults that should appear after the analyzer is run on the sources + protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// + /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, + /// then verifies each of them. + /// + /// An array of strings to create source documents from to run the analyzers on + /// The language of the classes represented by the source strings + /// The analyzer to be run on the source code + /// DiagnosticResults that should appear after the analyzer is run on the sources + private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) + { + var diagnostics = GetSortedDiagnostics(sources, language, analyzer); + VerifyDiagnosticResults(diagnostics, analyzer, expected); + } + + #endregion + + #region Actual comparisons and verifications + /// + /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. + /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. + /// + /// The Diagnostics found by the compiler after running the analyzer on the source code + /// The analyzer that was being run on the sources + /// Diagnostic Results that should have appeared in the code + private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) + { + int expectedCount = expectedResults.Count(); + int actualCount = actualResults.Count(); + + if (expectedCount != actualCount) + { + string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; + + Assert.True(false, + string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); + } + + for (int i = 0; i < expectedResults.Length; i++) + { + var actual = actualResults.ElementAt(i); + var expected = expectedResults[i]; + + if (expected.Line == -1 && expected.Column == -1) + { + if (actual.Location != Location.None) + { + Assert.True(false, + string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", + FormatDiagnostics(analyzer, actual))); + } + } + else + { + VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); + var additionalLocations = actual.AdditionalLocations.ToArray(); + + if (additionalLocations.Length != expected.Locations.Length - 1) + { + Assert.True(false, + string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", + expected.Locations.Length - 1, additionalLocations.Length, + FormatDiagnostics(analyzer, actual))); + } + + for (int j = 0; j < additionalLocations.Length; ++j) + { + VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); + } + } + + if (actual.Id != expected.Id) + { + Assert.True(false, + string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); + } + + if (actual.Severity != expected.Severity) + { + Assert.True(false, + string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); + } + + if (actual.GetMessage() != expected.Message) + { + Assert.True(false, + string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); + } + } + } + + /// + /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. + /// + /// The analyzer that was being run on the sources + /// The diagnostic that was found in the code + /// The Location of the Diagnostic found in the code + /// The DiagnosticResultLocation that should have been found + private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) + { + var actualSpan = actual.GetLineSpan(); + + Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), + string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); + + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + Assert.True(false, + string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + Assert.True(false, + string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + } + #endregion + + #region Formatting Diagnostics + /// + /// Helper method to format a Diagnostic into an easily readable string + /// + /// The analyzer that this verifier tests + /// The Diagnostics to be formatted + /// The Diagnostics formatted as a string + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + { + var builder = new StringBuilder(); + for (int i = 0; i < diagnostics.Length; ++i) + { + builder.AppendLine("// " + diagnostics[i].ToString()); + + var analyzerType = analyzer.GetType(); + var rules = analyzer.SupportedDiagnostics; + + foreach (var rule in rules) + { + if (rule != null && rule.Id == diagnostics[i].Id) + { + var location = diagnostics[i].Location; + if (location == Location.None) + { + builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); + } + else + { + Assert.True(location.IsInSource, + $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); + + string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; + var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; + + builder.AppendFormat("{0}({1}, {2}, {3}.{4})", + resultMethodName, + linePosition.Line + 1, + linePosition.Character + 1, + analyzerType.Name, + rule.Id); + } + + if (i != diagnostics.Length - 1) + { + builder.Append(','); + } + + builder.AppendLine(); + break; + } + } + } + return builder.ToString(); + } + #endregion + } +} diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 9e734641c..bf2457187 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -14,6 +14,7 @@ +
From a384ce02abcca096cb99d55f7ba9b52c2ec48148 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 13 Jan 2018 23:20:04 -0500 Subject: [PATCH 29/31] Support listening/watching activity types This resolves #931 As part of this change, StreamingType has been refactored to realign with how Discord seems to define the 'type' field on activities now. StreamType is renamed to ActivityType, and the following properties have been changed: - NotStreaming -> Playing - Twitch -> Streaming Additionally, the StreamType property/parameter has been removed from StreamingGame, and moved up a scope to Game. Normal Games may now set their type, to line up with changes in Discord's official clients. --- .../Entities/Activities/ActivityType.cs | 10 ++++++++++ .../Entities/Activities/Game.cs | 4 +++- .../Entities/Activities/IActivity.cs | 9 ++------- .../Entities/Activities/StreamingGame.cs | 7 +++---- .../Entities/Users/StreamType.cs | 8 -------- src/Discord.Net.Rest/API/Common/Game.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 2 +- .../DiscordShardedClient.cs | 12 +++++------ .../DiscordSocketClient.cs | 20 +++++++++---------- .../Extensions/EntityExtensions.cs | 5 ++--- 10 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/ActivityType.cs delete mode 100644 src/Discord.Net.Core/Entities/Users/StreamType.cs diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs new file mode 100644 index 000000000..c7db7b247 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum ActivityType + { + Playing = 0, + Streaming = 1, + Listening = 2, + Watching = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index f2b7e8eb6..fe32470ee 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -6,11 +6,13 @@ namespace Discord public class Game : IActivity { public string Name { get; internal set; } + public ActivityType Type { get; internal set; } internal Game() { } - public Game(string name) + public Game(string name, ActivityType type = ActivityType.Playing) { Name = name; + Type = type; } public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs index 0dcf34273..1f158217d 100644 --- a/src/Discord.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord +namespace Discord { public interface IActivity { string Name { get; } + ActivityType Type { get; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs index 140024272..afbc24cd9 100644 --- a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -6,15 +6,14 @@ namespace Discord public class StreamingGame : Game { public string Url { get; internal set; } - public StreamType StreamType { get; internal set; } - public StreamingGame(string name, string url, StreamType streamType) + public StreamingGame(string name, string url) { Name = name; Url = url; - StreamType = streamType; + Type = ActivityType.Streaming; } - + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Url})"; } diff --git a/src/Discord.Net.Core/Entities/Users/StreamType.cs b/src/Discord.Net.Core/Entities/Users/StreamType.cs deleted file mode 100644 index 7622e3d6e..000000000 --- a/src/Discord.Net.Core/Entities/Users/StreamType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum StreamType - { - NotStreaming = 0, - Twitch = 1 - } -} diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index bfb861692..29e0d987d 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -12,7 +12,7 @@ namespace Discord.API [JsonProperty("url")] public Optional StreamUrl { get; set; } [JsonProperty("type")] - public Optional StreamType { get; set; } + public Optional Type { get; set; } [JsonProperty("details")] public Optional Details { get; set; } [JsonProperty("state")] diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 2ab244aeb..5fa3cbff8 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -44,7 +44,7 @@ namespace Discord.WebSocket /// public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); - public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 4e99ae28d..fb78a201f 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -238,13 +238,13 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } - public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { IActivity activity = null; - if (streamUrl != null) - activity = new StreamingGame(name, streamUrl, streamType); - else if (name != null) - activity = new Game(name); + if (!string.IsNullOrEmpty(streamUrl)) + activity = new StreamingGame(name, streamUrl); + else if (!string.IsNullOrEmpty(name)) + activity = new Game(name, type); await SetActivityAsync(activity).ConfigureAwait(false); } public override async Task SetActivityAsync(IActivity activity) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index cb3f23c57..c220c337b 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -326,12 +326,12 @@ namespace Discord.WebSocket _statusSince = null; await SendStatusAsync().ConfigureAwait(false); } - public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { if (!string.IsNullOrEmpty(streamUrl)) - Activity = new StreamingGame(name, streamUrl, streamType); + Activity = new StreamingGame(name, streamUrl); else if (!string.IsNullOrEmpty(name)) - Activity = new Game(name); + Activity = new Game(name, type); else Activity = null; await SendStatusAsync().ConfigureAwait(false); @@ -354,15 +354,13 @@ namespace Discord.WebSocket // Discord only accepts rich presence over RPC, don't even bother building a payload if (Activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); - else if (Activity is StreamingGame stream) - { - gameModel.StreamUrl = stream.Url; - gameModel.StreamType = stream.StreamType; - } - else if (Activity != null) + + if (Activity != null) { gameModel.Name = Activity.Name; - gameModel.StreamType = StreamType.NotStreaming; + gameModel.Type = Activity.Type; + if (Activity is StreamingGame streamGame) + gameModel.StreamUrl = streamGame.Url; } await ApiClient.SendStatusUpdateAsync( diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index c66163610..f85c89c71 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -27,11 +27,10 @@ { return new StreamingGame( model.Name, - model.StreamUrl.Value, - model.StreamType.Value.GetValueOrDefault()); + model.StreamUrl.Value); } // Normal Game - return new Game(model.Name); + return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing); } // (Small, Large) From 05cd1ff85bd4e13ea2aad276eebc09295c9621df Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 14 Jan 2018 19:39:26 -0500 Subject: [PATCH 30/31] Don't attempt to resolve permissions for invalid roles This resolves #824. Discord seems to have inconsistencies where a role can be deleted, but there will still be a few users who still have it in their `role_ids`. I was able to find this bug appearing in 11 members of a 10,000 member guild, so it would make sense that this is relatively rare, and it's why we hadn't noticed it previously. Since our permission resolution code is implementation agnostic, it operates on the user's RoleIds collection, which is what Discord sends us directly, and is not vaidated against the member's guild. In our permission resolution code, we make the assumption that Discord will always be telling us the truth with regard to a member's `role_ids`. This PR changes the behavior of permissions resolution to instead verify that the guild was able to return a role before attempting to resolve its permissions. --- src/Discord.Net.Core/Utils/Permissions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index 367926dd1..7b92c9d3e 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -133,9 +133,10 @@ namespace Discord ulong deniedPermissions = 0UL, allowedPermissions = 0UL; foreach (var roleId in user.RoleIds) { - if (roleId != guild.EveryoneRole.Id) + IRole role = null; + if (roleId != guild.EveryoneRole.Id && (role = guild.GetRole(roleId)) != null) { - perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); + perms = channel.GetPermissionOverwrite(role); if (perms != null) { allowedPermissions |= perms.Value.AllowValue; From 73ac9d7886aa48b9d809c56e51945056f3b67232 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 27 Jan 2018 16:08:01 -0500 Subject: [PATCH 31/31] Remove incomplete reconnect handler for certain session invalidations This resolves #938 and #883 Note: This fix is not 'verified' in production (I waited over a week for another full Discord outage and never encountered one), but I do have it on record from b1nzy that Discord may send an OP9 with `{"d": true}` during outages, so this would appear to be the proper fix. The removed code seems to have been leftover from when ConnectionManager was rewritten and never finished. --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c220c337b..142f24417 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -416,11 +416,8 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - bool retry = (bool)payload; - if (retry) - _connection.Reconnect(); //TODO: Untested - else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: