From 8df2c1a1fbc0478f3af6398b705513ad6e8ca20a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 9 Oct 2018 19:26:57 -0400 Subject: [PATCH 01/26] fix: don't assume the member will always be included on MESSAGE_CREATE (#1167) * fix: don't assume the member will always be included on MESSAGE_CREATE This resolves #1153. Member objects are only included on a message when the user has transitioned from an offline state to an online state (i think?), so this change will fall back to the prior behavior, where we just create an incomplete member object for these states. * lint: use a ternary in place of an if/else block --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 81538e6e7..3d260d1a6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1158,7 +1158,11 @@ namespace Discord.WebSocket if (author == null) { if (guild != null) - author = guild.AddOrUpdateUser(data.Member.Value); //per g250k, we can create an entire member now + { + author = data.Member.IsSpecified // member isn't always included, but use it when we can + ? guild.AddOrUpdateUser(data.Member.Value) + : guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } else if (channel is SocketGroupChannel) author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); else From f3b9f358994b4f6ec3b8c78cd6f1eddaf79888fe Mon Sep 17 00:00:00 2001 From: Rithari Date: Fri, 12 Oct 2018 22:19:03 +0200 Subject: [PATCH 02/26] Add check for bot messages. Added a check so bot messages do not trigger any commands involuntariy. --- docs/guides/commands/samples/intro/command_handler.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/guides/commands/samples/intro/command_handler.cs b/docs/guides/commands/samples/intro/command_handler.cs index e4531fa41..efb31f9b9 100644 --- a/docs/guides/commands/samples/intro/command_handler.cs +++ b/docs/guides/commands/samples/intro/command_handler.cs @@ -35,9 +35,10 @@ public class CommandHandler // Create a number to track where the prefix ends and the command begins int argPos = 0; - // Determine if the message is a command based on the prefix + // Determine if the message is a command based on the prefix and make sure no bots trigger commands if (!(message.HasCharPrefix('!', ref argPos) || - message.HasMentionPrefix(_client.CurrentUser, ref argPos))) + message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) return; // Create a WebSocket-based command context based on the message @@ -60,4 +61,4 @@ public class CommandHandler // if (!result.IsSuccess) // await context.Channel.SendMessageAsync(result.ErrorReason); } -} \ No newline at end of file +} From 96fbb43f770bbd6b98a9b7fda9b7e0109d8738a6 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Fri, 19 Oct 2018 16:41:27 -0400 Subject: [PATCH 03/26] docs: Update joining_audio.cs (#1176) --- docs/guides/voice/samples/joining_audio.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs index 4cec67540..3abbfb632 100644 --- a/docs/guides/voice/samples/joining_audio.cs +++ b/docs/guides/voice/samples/joining_audio.cs @@ -1,4 +1,5 @@ -[Command("join")] +// The command's Run Mode MUST be set to RunMode.Async, otherwise, being connected to a voice channel will block the gateway thread. +[Command("join", RunMode = RunMode.Async)] public async Task JoinChannel(IVoiceChannel channel = null) { // Get the audio channel From 10f67a8098f124fbf211d584f50699e9127136cd Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 19 Oct 2018 17:18:59 -0400 Subject: [PATCH 04/26] feature: consolidate all results into CommandExecuted (#1164) * feature: consolidate all results into CommandExecuted This resolves #694. This is a breaking change! - changes the signature of CommandExecuted from (CommandInfo, ...) to (Optional, ...), since we are now including Search result failures in the event (and a command isn't accessible yet). * lint: remove unfinished thoughts --- src/Discord.Net.Commands/CommandService.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 11f4ce276..432b75f27 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -49,8 +49,8 @@ namespace Discord.Commands /// Should the command encounter any of the aforementioned error, this event will not be raised. /// /// - public event Func CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } - internal readonly AsyncEvent> _commandExecutedEvent = new AsyncEvent>(); + public event Func, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } + internal readonly AsyncEvent, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent, ICommandContext, IResult, Task>>(); private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; @@ -512,7 +512,11 @@ namespace Discord.Commands var searchResult = Search(input); if (!searchResult.IsSuccess) + { + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); return searchResult; + } + var commands = searchResult.Commands; var preconditionResults = new Dictionary(); @@ -532,6 +536,8 @@ namespace Discord.Commands var bestCandidate = preconditionResults .OrderByDescending(x => x.Key.Command.Priority) .FirstOrDefault(x => !x.Value.IsSuccess); + + await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); return bestCandidate.Value; } @@ -589,12 +595,17 @@ namespace Discord.Commands //All parses failed, return the one from the highest priority command, using score as a tie breaker var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); + + await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); return bestMatch.Value; } //If we get this far, at least one parse was successful. Execute the most likely overload. var chosenOverload = successfulParses[0]; - return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); + var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); + if (!result.IsSuccess) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); + return result; } } } From d30d12246d5173321c2556cf65d3c69da230cb8c Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 19 Oct 2018 14:20:41 -0700 Subject: [PATCH 05/26] Update Guild and Message Models (#1165) * Add ExplicitContentFilter property to Guild * re-order properties to match order listed on api docs * re-order SystemChannelId to match api docs * Implement ApplicationId in Guild model * Add ExplicitContentFilter property to Guild * re-order properties to match order listed on api docs * re-order SystemChannelId to match api docs * Implement ApplicationId in Guild model * Improve xmldoc for IGuild ExplicitContentFilter * Update xmldoc * docs "Id" -> "ID" * rename Test.GuildPermissions to a more general Test.Guilds * Add ExplicitContentFilter to GuildProperties * Add a test for ExplicitContentFilterLevel modification behavior * Implement ModifyAsync behavior * simplify ExplicitContentFilter test * Add RestGuild ApplicationId inheritdoc * Implement message Activity and Application model update * RestMessage Application and Activity implementation * add ToString to MessageApplication * Add IconUrl property to MessageApplication * clean up whitespace * another excessive whitespace removal --- .../Guilds/ExplicitContentFilterLevel.cs | 13 ++++++ .../Entities/Guilds/GuildProperties.cs | 4 ++ .../Entities/Guilds/IGuild.cs | 14 ++++++ .../Entities/Messages/IMessage.cs | 20 +++++++++ .../Entities/Messages/MessageActivity.cs | 27 ++++++++++++ .../Entities/Messages/MessageActivityType.cs | 16 +++++++ .../Entities/Messages/MessageApplication.cs | 43 +++++++++++++++++++ src/Discord.Net.Rest/API/Common/Guild.cs | 14 +++--- src/Discord.Net.Rest/API/Common/Message.cs | 6 +++ .../API/Common/MessageActivity.cs | 17 ++++++++ .../API/Common/MessageApplication.cs | 38 ++++++++++++++++ .../API/Rest/ModifyGuildParams.cs | 4 +- .../Entities/Guilds/GuildHelper.cs | 6 ++- .../Entities/Guilds/RestGuild.cs | 6 +++ .../Entities/Messages/RestMessage.cs | 27 ++++++++++++ .../Entities/Guilds/SocketGuild.cs | 6 +++ .../Entities/Messages/SocketMessage.cs | 30 +++++++++++++ ...ts.GuildPermissions.cs => Tests.Guilds.cs} | 19 +++++++- 18 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs create mode 100644 src/Discord.Net.Core/Entities/Messages/MessageActivity.cs create mode 100644 src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs create mode 100644 src/Discord.Net.Core/Entities/Messages/MessageApplication.cs create mode 100644 src/Discord.Net.Rest/API/Common/MessageActivity.cs create mode 100644 src/Discord.Net.Rest/API/Common/MessageApplication.cs rename test/Discord.Net.Tests/{Tests.GuildPermissions.cs => Tests.Guilds.cs} (94%) diff --git a/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs b/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs new file mode 100644 index 000000000..54c0bdafe --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/ExplicitContentFilterLevel.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public enum ExplicitContentFilterLevel + { + /// No messages will be scanned. + Disabled = 0, + /// Scans messages from all guild members that do not have a role. + /// Recommented option for servers that use roles for trusted membership. + MembersWithoutRoles = 1, + /// Scan messages sent by all guild members. + AllMembers = 2 + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 0ffe8db35..e6d21a463 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -66,5 +66,9 @@ namespace Discord /// Gets or sets the ID of the owner of this guild. /// public Optional OwnerId { get; set; } + /// + /// Gets or sets the explicit content filter level of this guild. + /// + public Optional ExplicitContentFilter { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 9b91b9440..c321cd2e3 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -53,6 +53,13 @@ namespace Discord /// VerificationLevel VerificationLevel { get; } /// + /// Gets the level of content filtering applied to user's content in a Guild. + /// + /// + /// The level of explicit content filtering. + /// + ExplicitContentFilterLevel ExplicitContentFilter { get; } + /// /// Gets the ID of this guild's icon. /// /// @@ -141,6 +148,13 @@ namespace Discord /// ulong OwnerId { get; } /// + /// Gets the application ID of the guild creator if it is bot-created. + /// + /// + /// A representing the snowflake identifier of the application ID that created this guild, or null if it was not bot-created. + /// + ulong? ApplicationId { get; } + /// /// Gets the ID of the region hosting this guild's voice channels. /// /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 87754eecd..33e019419 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -100,5 +100,25 @@ namespace Discord /// A read-only collection of user IDs. /// IReadOnlyCollection MentionedUserIds { get; } + /// + /// Returns the Activity associated with a message. + /// + /// + /// Sent with Rich Presence-related chat embeds. + /// + /// + /// A message's activity, if any is associated. + /// + MessageActivity Activity { get; } + /// + /// Returns the Application associated with a messsage. + /// + /// + /// Sent with Rich-Presence-related chat embeds. + /// + /// + /// A message's application, if any is associated. + /// + MessageApplication Application { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs new file mode 100644 index 000000000..d19e6a8e9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageActivity + { + /// + /// Gets the type of activity of this message. + /// + public MessageActivityType Type { get; set; } + /// + /// Gets the party ID of this activity, if any. + /// + public string PartyId { get; set; } + + private string DebuggerDisplay + => $"{Type}{(string.IsNullOrWhiteSpace(PartyId) ? "" : $" {PartyId}")}"; + + public override string ToString() => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs b/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs new file mode 100644 index 000000000..68b99a9c1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageActivityType.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum MessageActivityType + { + Join = 1, + Spectate = 2, + Listen = 3, + JoinRequest = 5 + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs new file mode 100644 index 000000000..960d1700f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + public ulong Id { get; set; } + /// + /// Gets the ID of the embed's image asset. + /// + public string CoverImage { get; set; } + /// + /// Gets the application's description. + /// + public string Description { get; set; } + /// + /// Gets the ID of the application's icon. + /// + public string Icon { get; set; } + /// + /// Gets the Url of the application's icon. + /// + public string IconUrl + => $"https://cdn.discordapp.com/app-icons/{Id}/{Icon}"; + /// + /// Gets the name of the application. + /// + public string Name { get; set; } + private string DebuggerDisplay + => $"{Name} ({Id}): {Description}"; + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index 0ca1bc236..a84b55a93 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -25,10 +25,12 @@ namespace Discord.API public bool EmbedEnabled { get; set; } [JsonProperty("embed_channel_id")] public ulong? EmbedChannelId { get; set; } - [JsonProperty("system_channel_id")] - public ulong? SystemChannelId { get; set; } [JsonProperty("verification_level")] public VerificationLevel VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public DefaultMessageNotifications DefaultMessageNotifications { get; set; } + [JsonProperty("explicit_content_filter")] + public ExplicitContentFilterLevel ExplicitContentFilter { get; set; } [JsonProperty("voice_states")] public VoiceState[] VoiceStates { get; set; } [JsonProperty("roles")] @@ -39,7 +41,9 @@ namespace Discord.API public string[] Features { get; set; } [JsonProperty("mfa_level")] public MfaLevel MfaLevel { get; set; } - [JsonProperty("default_message_notifications")] - public DefaultMessageNotifications DefaultMessageNotifications { get; set; } + [JsonProperty("application_id")] + public ulong? ApplicationId { get; set; } + [JsonProperty("system_channel_id")] + public ulong? SystemChannelId { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index 229249ccf..10bdbe568 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -44,5 +44,11 @@ namespace Discord.API public Optional Pinned { get; set; } [JsonProperty("reactions")] public Optional Reactions { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("activity")] + public Optional Activity { get; set; } + // sent with Rich Presence-related chat embeds + [JsonProperty("application")] + public Optional Application { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageActivity.cs b/src/Discord.Net.Rest/API/Common/MessageActivity.cs new file mode 100644 index 000000000..701f6fc03 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageActivity.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageActivity + { + [JsonProperty("type")] + public Optional Type { get; set; } + [JsonProperty("party_id")] + public Optional PartyId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageApplication.cs b/src/Discord.Net.Rest/API/Common/MessageApplication.cs new file mode 100644 index 000000000..7302185ad --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageApplication.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class MessageApplication + { + /// + /// Gets the snowflake ID of the application. + /// + [JsonProperty("id")] + public ulong Id { get; set; } + /// + /// Gets the ID of the embed's image asset. + /// + [JsonProperty("cover_image")] + public string CoverImage { get; set; } + /// + /// Gets the application's description. + /// + [JsonProperty("description")] + public string Description { get; set; } + /// + /// Gets the ID of the application's icon. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + /// + /// Gets the name of the application. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index 8de10f534..ba70c58d6 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -28,5 +28,7 @@ namespace Discord.API.Rest public Optional AfkChannelId { get; set; } [JsonProperty("owner_id")] public Optional OwnerId { get; set; } + [JsonProperty("explicit_content_filter")] + public Optional ExplicitContentFilter { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index b8b66f802..7b77dafc7 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -32,7 +32,8 @@ namespace Discord.Rest Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), - VerificationLevel = args.VerificationLevel + VerificationLevel = args.VerificationLevel, + ExplicitContentFilter = args.ExplicitContentFilter }; if (args.AfkChannel.IsSpecified) @@ -60,6 +61,9 @@ namespace Discord.Rest if (!apiArgs.Icon.IsSpecified && guild.IconId != null) apiArgs.Icon = new ImageModel(guild.IconId); + if (args.ExplicitContentFilter.IsSpecified) + apiArgs.ExplicitContentFilter = args.ExplicitContentFilter.Value; + return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } /// is null. diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 9c4ecd848..8cd81f218 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -32,6 +32,8 @@ namespace Discord.Rest public MfaLevel MfaLevel { get; private set; } /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + /// + public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } /// public ulong? AFKChannelId { get; private set; } @@ -48,6 +50,8 @@ namespace Discord.Rest /// public string SplashId { get; private set; } internal bool Available { get; private set; } + /// + public ulong? ApplicationId { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -98,6 +102,8 @@ namespace Discord.Rest VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; DefaultMessageNotifications = model.DefaultMessageNotifications; + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; if (model.Emojis != null) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index a4ecf3647..fae1aff99 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -55,6 +55,10 @@ namespace Discord.Rest /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); + /// + public MessageActivity Activity { get; private set; } + /// + public MessageApplication Application { get; private set; } internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) @@ -77,6 +81,29 @@ namespace Discord.Rest if (model.Content.IsSpecified) Content = model.Content.Value; + + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.Value + }; + } } /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index b4f87e12a..ce8cd08de 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -50,6 +50,8 @@ namespace Discord.WebSocket public MfaLevel MfaLevel { get; private set; } /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + /// + public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } /// /// Gets the number of members. /// @@ -73,6 +75,8 @@ namespace Discord.WebSocket internal bool IsAvailable { get; private set; } /// Indicates whether the client is connected to this guild. public bool IsConnected { get; internal set; } + /// + public ulong? ApplicationId { get; internal set; } internal ulong? AFKChannelId { get; private set; } internal ulong? EmbedChannelId { get; private set; } @@ -346,6 +350,8 @@ namespace Discord.WebSocket VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; DefaultMessageNotifications = model.DefaultMessageNotifications; + ExplicitContentFilter = model.ExplicitContentFilter; + ApplicationId = model.ApplicationId; if (model.Emojis != null) { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 0767f2ad7..2cfcee270 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -43,6 +43,13 @@ namespace Discord.WebSocket public virtual bool IsPinned => false; /// public virtual DateTimeOffset? EditedTimestamp => null; + + /// + public MessageActivity Activity { get; private set; } + + /// + public MessageApplication Application { get; private set; } + /// /// Returns all attachments included in this message. /// @@ -105,6 +112,29 @@ namespace Discord.WebSocket if (model.Content.IsSpecified) Content = model.Content.Value; + + if (model.Application.IsSpecified) + { + // create a new Application from the API model + Application = new MessageApplication() + { + Id = model.Application.Value.Id, + CoverImage = model.Application.Value.CoverImage, + Description = model.Application.Value.Description, + Icon = model.Application.Value.Icon, + Name = model.Application.Value.Name + }; + } + + if (model.Activity.IsSpecified) + { + // create a new Activity from the API model + Activity = new MessageActivity() + { + Type = model.Activity.Value.Type.Value, + PartyId = model.Activity.Value.PartyId.Value + }; + } } /// diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.Guilds.cs similarity index 94% rename from test/Discord.Net.Tests/Tests.GuildPermissions.cs rename to test/Discord.Net.Tests/Tests.Guilds.cs index f49f431b5..09e3d044d 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.Guilds.cs @@ -5,8 +5,25 @@ using Xunit; namespace Discord { - public class GuidPermissionsTests + public partial class Tests { + /// + /// Tests the behavior of modifying the ExplicitContentFilter property of a Guild. + /// + [Fact] + public async Task TestExplicitContentFilter() + { + foreach (var level in Enum.GetValues(typeof(ExplicitContentFilterLevel))) + { + await _guild.ModifyAsync(x => x.ExplicitContentFilter = (ExplicitContentFilterLevel)level); + await _guild.UpdateAsync(); + Assert.Equal(level, _guild.ExplicitContentFilter); + } + } + + /// + /// Tests the behavior of the GuildPermissions class. + /// [Fact] public Task TestGuildPermission() { From 5ea1fb374e1fdb65d4410a5744434fbc01b39543 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 20 Oct 2018 05:21:37 +0800 Subject: [PATCH 06/26] Add SyncPermissionsAsync to Sync Child Channels with its Parent (#1159) * Initial implementation * Adjust according to comments See: https://github.com/RogueException/Discord.Net/pull/1159/files/6e76b45713ae95cf6fdfb96b57ed7095f6b6cc59#diff-58466c35787d448266d026692e467baa --- .../Entities/Channels/INestedChannel.cs | 5 +++++ src/Discord.Net.Rest/API/Common/Overwrite.cs | 2 +- .../API/Rest/ModifyGuildChannelParams.cs | 4 +++- .../Entities/Channels/ChannelHelper.cs | 17 +++++++++++++++++ .../Entities/Channels/RestTextChannel.cs | 2 ++ .../Entities/Channels/RestVoiceChannel.cs | 2 ++ .../Entities/Channels/SocketTextChannel.cs | 2 ++ .../Entities/Channels/SocketVoiceChannel.cs | 2 ++ 8 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index 22182a4ca..6719f91d4 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -25,5 +25,10 @@ namespace Discord /// representing the parent of this channel; null if none is set. /// Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + + /// + /// Syncs the permissions of this nested channel with its parent's. + /// + Task SyncPermissionsAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Common/Overwrite.cs b/src/Discord.Net.Rest/API/Common/Overwrite.cs index 1ba836127..1f3548a1c 100644 --- a/src/Discord.Net.Rest/API/Common/Overwrite.cs +++ b/src/Discord.Net.Rest/API/Common/Overwrite.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index 120eeb3a8..e5e8a4632 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -12,5 +12,7 @@ namespace Discord.API.Rest public Optional Position { get; set; } [JsonProperty("parent_id")] public Optional CategoryId { get; set; } + [JsonProperty("permission_overwrites")] + public Optional Overwrites { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 716f3beaf..65b4869b8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -348,6 +348,23 @@ namespace Discord.Rest var model = await client.ApiClient.GetChannelAsync(channel.CategoryId.Value, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, model) as ICategoryChannel; } + public static async Task SyncPermissionsAsync(INestedChannel channel, BaseDiscordClient client, RequestOptions options) + { + var category = await GetCategoryAsync(channel, client, options).ConfigureAwait(false); + if (category == null) throw new InvalidOperationException("This channel does not have a parent channel."); + + var apiArgs = new ModifyGuildChannelParams + { + Overwrites = category.PermissionOverwrites + .Select(overwrite => new API.Overwrite{ + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + }; + await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } //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 4ccd35a3a..beeb8de19 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -201,6 +201,8 @@ namespace Discord.Rest /// public Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 7f0295c18..1a2c5ceae 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -57,6 +57,8 @@ namespace Discord.Rest /// public Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 8faf1c6eb..acd868020 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -31,6 +31,8 @@ namespace Discord.WebSocket /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); private bool _nsfw; /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 07977e5e0..dd71416db 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -30,6 +30,8 @@ namespace Discord.WebSocket /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); /// public override IReadOnlyCollection Users From fe07a83f1f5f8da9886f426fe21f038b61693123 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 19 Oct 2018 17:22:02 -0400 Subject: [PATCH 07/26] fix: strip trailing slash from ratelimit bucket IDs (#1163) * fix: strip trailing slash from ratelimit bucket IDs This resolves #1125. fixes a bug where some ratelimit buckets would include a trailing slash, while others wouldn't; this would cause them to be treated as separate ratelimits, even though they are the same ideally this fix should change the ratelimit generator, but that code is pretty complicated and this was an easier fix that seems less likely to break things in the future. tested against normal bot function, all routes are assigned the proper buckets from my testing, so this should be good to go. * lint: use more performant algorithm, operate on StringBuilder --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 36406bfea..284a51756 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,3 +1,4 @@ + #pragma warning disable CS1591 using Discord.API.Rest; using Discord.Net; @@ -1426,8 +1427,11 @@ namespace Discord.API lastIndex = rightIndex + 1; } + if (builder[builder.Length - 1] == '/') + builder.Remove(builder.Length - 1, 1); format = builder.ToString(); + return x => string.Format(format, x.ToArray()); } catch (Exception ex) From 88e66860a6c3131f0a012e5f3a9fa80ca3c55ff1 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 19 Oct 2018 17:29:45 -0400 Subject: [PATCH 08/26] docs: update command sample to use CommandExecuted, Log --- .../Services/CommandHandlingService.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/samples/02_commands_framework/Services/CommandHandlingService.cs b/samples/02_commands_framework/Services/CommandHandlingService.cs index fc253eed3..ca7af2774 100644 --- a/samples/02_commands_framework/Services/CommandHandlingService.cs +++ b/samples/02_commands_framework/Services/CommandHandlingService.cs @@ -20,6 +20,8 @@ namespace _02_commands_framework.Services _discord = services.GetRequiredService(); _services = services; + _commands.CommandExecuted += CommandExecutedAsync; + _commands.Log += LogAsync; _discord.MessageReceived += MessageReceivedAsync; } @@ -39,11 +41,28 @@ namespace _02_commands_framework.Services if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return; var context = new SocketCommandContext(_discord, message); - var result = await _commands.ExecuteAsync(context, argPos, _services); + await _commands.ExecuteAsync(context, argPos, _services); // we will handle the result in CommandExecutedAsync + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + // command is unspecified when there was a search failure (command not found); we don't care about these errors + if (!command.IsSpecified) + return; + + // the command was succesful, we don't care about this result, unless we want to log that a command succeeded. + if (result.IsSuccess) + return; + + // the command failed, let's notify the user that something happened. + await context.Channel.SendMessageAsync($"error: {result.ToString()}"); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); - if (result.Error.HasValue && - result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors - await context.Channel.SendMessageAsync(result.ToString()); + return Task.CompletedTask; } } } From c7e7f7e51a0afc3c539d1b7876b2341701f731ff Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 19 Oct 2018 17:52:36 -0400 Subject: [PATCH 09/26] docs: add explanation to RunMode.Async in voice docs --- docs/guides/voice/sending-voice.md | 69 +++++++++++++++++------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index c30805836..476f2f42e 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -11,16 +11,16 @@ Information is not guaranteed to be accurate. ## Installing -Audio requires two native libraries, `libsodium` and `opus`. -Both of these libraries must be placed in the runtime directory of your -bot. (When developing on .NET Framework, this would be `bin/debug`, -when developing on .NET Core, this is where you execute `dotnet run` +Audio requires two native libraries, `libsodium` and `opus`. +Both of these libraries must be placed in the runtime directory of your +bot. (When developing on .NET Framework, this would be `bin/debug`, +when developing on .NET Core, this is where you execute `dotnet run` from; typically the same directory as your csproj). -For Windows Users, precompiled binaries are available for your +For Windows Users, precompiled binaries are available for your convienence [here](https://discord.foxbot.me/binaries/). -For Linux Users, you will need to compile [Sodium] and [Opus] from +For Linux Users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager. [Sodium]: https://download.libsodium.org/libsodium/releases/ @@ -28,7 +28,7 @@ source, or install them from your package manager. ## Joining a Channel -Joining a channel is the first step to sending audio, and will return +Joining a channel is the first step to sending audio, and will return an [IAudioClient] to send data with. To join a channel, simply await [ConnectAsync] on any instance of an @@ -36,67 +36,76 @@ To join a channel, simply await [ConnectAsync] on any instance of an [!code-csharp[Joining a Channel](samples/joining_audio.cs)] -The client will sustain a connection to this channel until it is +>[!WARNING] +>Commands which mutate voice states, such as those where you join/leave +>an audio channel, or send audio, should use [RunMode.Async]. RunMode.Async +>is necessary to prevent a feedback loop which will deadlock clients +>in their default configuration. If you know that you're running your +>commands in a different task than the gateway task, RunMode.Async is +>not required. + +The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to disconnect. -It should be noted that voice connections are created on a per-guild -basis; only one audio connection may be open by the bot in a single -guild. To switch channels within a guild, invoke [ConnectAsync] on +It should be noted that voice connections are created on a per-guild +basis; only one audio connection may be open by the bot in a single +guild. To switch channels within a guild, invoke [ConnectAsync] on another voice channel in the guild. [IAudioClient]: xref:Discord.Audio.IAudioClient [ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* +[RunMode.Async]: xref:Discord.Commands.RunMode ## Transmitting Audio ### With FFmpeg -[FFmpeg] is an open source, highly versatile AV-muxing tool. This is +[FFmpeg] is an open source, highly versatile AV-muxing tool. This is the recommended method of transmitting audio. -Before you begin, you will need to have a version of FFmpeg downloaded -and placed somewhere in your PATH (or alongside the bot, in the same -location as libsodium and opus). Windows binaries are available on +Before you begin, you will need to have a version of FFmpeg downloaded +and placed somewhere in your PATH (or alongside the bot, in the same +location as libsodium and opus). Windows binaries are available on [FFmpeg's download page]. [FFmpeg]: https://ffmpeg.org/ [FFmpeg's download page]: https://ffmpeg.org/download.html -First, you will need to create a Process that starts FFmpeg. An -example of how to do this is included below, though it is important +First, you will need to create a Process that starts FFmpeg. An +example of how to do this is included below, though it is important that you return PCM at 48000hz. >[!NOTE] ->As of the time of this writing, Discord.Audio struggles significantly ->with processing audio that is already opus-encoded; you will need to +>As of the time of this writing, Discord.Audio struggles significantly +>with processing audio that is already opus-encoded; you will need to >use the PCM write streams. [!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] -Next, to transmit audio from FFmpeg to Discord, you will need to -pull an [AudioOutStream] from your [IAudioClient]. Since we're using +Next, to transmit audio from FFmpeg to Discord, you will need to +pull an [AudioOutStream] from your [IAudioClient]. Since we're using PCM audio, use [IAudioClient.CreatePCMStream]. -The sample rate argument doesn't particularly matter, so long as it is -a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of +The sample rate argument doesn't particularly matter, so long as it is +a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of simplicity, I recommend using 1920. -Channels should be left at `2`, unless you specified a different value +Channels should be left at `2`, unless you specified a different value for `-ac 2` when creating FFmpeg. [AudioOutStream]: xref:Discord.Audio.AudioOutStream [IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ -Finally, audio will need to be piped from FFmpeg's stdout into your -AudioOutStream. This step can be as complex as you'd like it to be, but -for the majority of cases, you can just use [Stream.CopyToAsync], as +Finally, audio will need to be piped from FFmpeg's stdout into your +AudioOutStream. This step can be as complex as you'd like it to be, but +for the majority of cases, you can just use [Stream.CopyToAsync], as shown below. [Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx -If you are implementing a queue for sending songs, it's likely that -you will want to wait for audio to stop playing before continuing on -to the next song. You can await `AudioOutStream.FlushAsync` to wait for +If you are implementing a queue for sending songs, it's likely that +you will want to wait for audio to stop playing before continuing on +to the next song. You can await `AudioOutStream.FlushAsync` to wait for the audio client's internal buffer to clear out. [!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] From fb8dbcae4beef1684f22cf7d4c3cc3be0918d958 Mon Sep 17 00:00:00 2001 From: Rithari Date: Sat, 20 Oct 2018 15:59:06 +0200 Subject: [PATCH 10/26] Added check to ignore bot commands. (#1175) Added the usual check to ignore bot commands in this sample CommandHandler as seen and approved in #1174. --- .../commands/samples/post-execution/command_executed_demo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/commands/samples/post-execution/command_executed_demo.cs b/docs/guides/commands/samples/post-execution/command_executed_demo.cs index 8d8fb911b..b87f4ef06 100644 --- a/docs/guides/commands/samples/post-execution/command_executed_demo.cs +++ b/docs/guides/commands/samples/post-execution/command_executed_demo.cs @@ -27,7 +27,7 @@ public async Task HandleCommandAsync(SocketMessage msg) var message = messageParam as SocketUserMessage; if (message == null) return; int argPos = 0; - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || message.Author.IsBot) return; var context = new SocketCommandContext(_client, message); var result = await _commands.ExecuteAsync(context, argPos, _services); // Optionally, you may pass the result manually into your @@ -35,4 +35,4 @@ public async Task HandleCommandAsync(SocketMessage msg) // precondition failures in the same method. // await OnCommandExecutedAsync(null, context, result); -} \ No newline at end of file +} From 00097d3c271eab194853e2eed72a7bc495948967 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sat, 20 Oct 2018 10:02:25 -0400 Subject: [PATCH 11/26] Add DiscordShardedClient sample project & Client FAQ entry. (#1177) * Add DiscordShardedClient sample project & Client FAQ entry. * Revise language, fix typo, add xrefs * Adjust placement of message handler. * Resolve DI issue with initialized client; properly initialize command handling service. --- Discord.Net.sln | 17 ++++- docs/faq/basics/client-basics.md | 30 +++++++- .../03_sharded_client.csproj | 14 ++++ .../03_sharded_client/Modules/PublicModule.cs | 17 +++++ samples/03_sharded_client/Program.cs | 69 +++++++++++++++++++ .../Services/CommandHandlingService.cs | 52 ++++++++++++++ 6 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 samples/03_sharded_client/03_sharded_client.csproj create mode 100644 samples/03_sharded_client/Modules/PublicModule.cs create mode 100644 samples/03_sharded_client/Program.cs create mode 100644 samples/03_sharded_client/Services/CommandHandlingService.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index 9bb940d8c..245515c7c 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -26,10 +26,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "sr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "03_sharded_client", "samples\03_sharded_client\03_sharded_client.csproj", "{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -160,6 +162,18 @@ Global {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x64.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Debug|x86.Build.0 = Debug|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|Any CPU.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x64.Build.0 = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.ActiveCfg = Release|Any CPU + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -173,6 +187,7 @@ Global {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md index 5c529a3f5..9377ac2e9 100644 --- a/docs/faq/basics/client-basics.md +++ b/docs/faq/basics/client-basics.md @@ -63,4 +63,32 @@ use the cached message entity. Read more about it [here](xref:Guides.Concepts.Ev [MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize [DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated \ No newline at end of file +[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated + +## What is a shard/sharded client, and how is it different from the `DiscordSocketClient`? +As your bot grows in popularity, it is recommended that you should section your bot off into separate processes. +The [DiscordShardedClient] is essentially a class that allows you to easily create and manage multiple [DiscordSocketClient] +instances, with each one serving a different amount of guilds. + +There are very few differences from the [DiscordSocketClient] class, and it is very straightforward +to modify your existing code to use a [DiscordShardedClient] when necessary. + +1. You need to specify the total amount of shards, or shard ids, via [DiscordShardedClient]'s constructors. +2. The [Connected], [Disconnected], [Ready], and [LatencyUpdated] events + are replaced with [ShardConnected], [ShardDisconnected], [ShardReady], and [ShardLatencyUpdated]. +3. Every event handler you apply/remove to the [DiscordShardedClient] is applied/removed to each shard. + If you wish to control a specific shard's events, you can access an individual shard through the `Shards` property. + +If you do not wish to use the [DiscordShardedClient] and instead reuse the same [DiscordSocketClient] code and manually shard them, +you can do so by specifying the [ShardId] for the [DiscordSocketConfig] and pass that to the [DiscordSocketClient]'s constructor. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordShardedClient]: xref:Discord.WebSocket.DiscordShardedClient +[Connected]: xref:Discord.WebSocket.DiscordSocketClient.Connected +[Disconnected]: xref:Discord.WebSocket.DiscordSocketClient.Disconnected +[LatencyUpdated]: xref:Discord.WebSocket.DiscordSocketClient.LatencyUpdated +[ShardConnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardConnected +[ShardDisconnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardDisconnected +[ShardReady]: xref:Discord.WebSocket.DiscordShardedClient.ShardReady +[ShardLatencyUpdated]: xref:Discord.WebSocket.DiscordShardedClient.ShardLatencyUpdated +[ShardId]: xref:Discord.WebSocket.DiscordSocketConfig.ShardId diff --git a/samples/03_sharded_client/03_sharded_client.csproj b/samples/03_sharded_client/03_sharded_client.csproj new file mode 100644 index 000000000..5d76961cd --- /dev/null +++ b/samples/03_sharded_client/03_sharded_client.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp2.1 + _03_sharded_client + + + + + + + + diff --git a/samples/03_sharded_client/Modules/PublicModule.cs b/samples/03_sharded_client/Modules/PublicModule.cs new file mode 100644 index 000000000..60e57563a --- /dev/null +++ b/samples/03_sharded_client/Modules/PublicModule.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Discord.Commands; + +namespace _03_sharded_client.Modules +{ + // Remember to make your module reference the ShardedCommandContext + public class PublicModule : ModuleBase + { + [Command("info")] + public async Task InfoAsync() + { + var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards} shards! + This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; + await ReplyAsync(msg); + } + } +} diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs new file mode 100644 index 000000000..048145f9f --- /dev/null +++ b/samples/03_sharded_client/Program.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using _03_sharded_client.Services; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; + +namespace _03_sharded_client +{ + // This is a minimal example of using Discord.Net's Sharded Client + // The provided DiscordShardedClient class simplifies having multiple + // DiscordSocketClient instances (or shards) to serve a large number of guilds. + class Program + { + private DiscordShardedClient _client; + + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + public async Task MainAsync() + { + // You specify the amount of shards you'd like to have with the + // DiscordSocketConfig. Generally, it's recommended to + // have 1 shard per 1500-2000 guilds your bot is in. + var config = new DiscordSocketConfig + { + TotalShards = 2 + }; + + _client = new DiscordShardedClient(config); + var services = ConfigureServices(); + + // The Sharded Client does not have a Ready event. + // The ShardReady event is used instead, allowing for individual + // control per shard. + _client.ShardReady += ReadyAsync; + _client.Log += LogAsync; + + await services.GetRequiredService().InitializeAsync(); + + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await _client.StartAsync(); + + await Task.Delay(-1); + } + + private IServiceProvider ConfigureServices() + { + return new ServiceCollection() + .AddSingleton(_client) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + } + + + private Task ReadyAsync(DiscordSocketClient shard) + { + Console.WriteLine($"Shard Number {shard.ShardId} is connected and ready!"); + return Task.CompletedTask; + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + return Task.CompletedTask; + } + } +} diff --git a/samples/03_sharded_client/Services/CommandHandlingService.cs b/samples/03_sharded_client/Services/CommandHandlingService.cs new file mode 100644 index 000000000..31b70d4ae --- /dev/null +++ b/samples/03_sharded_client/Services/CommandHandlingService.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +namespace _03_sharded_client.Services +{ + public class CommandHandlingService + { + private readonly CommandService _commands; + private readonly DiscordShardedClient _discord; + private readonly IServiceProvider _services; + + public CommandHandlingService(IServiceProvider services) + { + _commands = services.GetRequiredService(); + _discord = services.GetRequiredService(); + _services = services; + } + + public async Task InitializeAsync() + { + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + _discord.MessageReceived += MessageReceivedAsync; + } + + public async Task MessageReceivedAsync(SocketMessage rawMessage) + { + // Ignore system messages, or messages from other bots + if (!(rawMessage is SocketUserMessage message)) + return; + if (message.Source != MessageSource.User) + return; + + // This value holds the offset where the prefix ends + var argPos = 0; + if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) + return; + + // A new kind of command context, ShardedCommandContext can be utilized with the commands framework + var context = new ShardedCommandContext(_discord, message); + var result = await _commands.ExecuteAsync(context, argPos, _services); + + if (result.Error.HasValue && + result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors + await context.Channel.SendMessageAsync(result.ToString()); + } + } +} From 07ec413884cc6b509acc6d8a28b10880fa7c9c47 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 20 Oct 2018 10:05:08 -0400 Subject: [PATCH 12/26] docs: update shard sample to use CommandExecuted, Log --- .../Services/CommandHandlingService.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/samples/03_sharded_client/Services/CommandHandlingService.cs b/samples/03_sharded_client/Services/CommandHandlingService.cs index 31b70d4ae..08a3887f0 100644 --- a/samples/03_sharded_client/Services/CommandHandlingService.cs +++ b/samples/03_sharded_client/Services/CommandHandlingService.cs @@ -19,6 +19,10 @@ namespace _03_sharded_client.Services _commands = services.GetRequiredService(); _discord = services.GetRequiredService(); _services = services; + + _commands.CommandExecuted += CommandExecutedAsync; + _commands.Log += LogAsync; + _discord.MessageReceived += MessageReceivedAsync; } public async Task InitializeAsync() @@ -42,11 +46,28 @@ namespace _03_sharded_client.Services // A new kind of command context, ShardedCommandContext can be utilized with the commands framework var context = new ShardedCommandContext(_discord, message); - var result = await _commands.ExecuteAsync(context, argPos, _services); + await _commands.ExecuteAsync(context, argPos, _services); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + // command is unspecified when there was a search failure (command not found); we don't care about these errors + if (!command.IsSpecified) + return; + + // the command was succesful, we don't care about this result, unless we want to log that a command succeeded. + if (result.IsSuccess) + return; + + // the command failed, let's notify the user that something happened. + await context.Channel.SendMessageAsync($"error: {result.ToString()}"); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); - if (result.Error.HasValue && - result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors - await context.Channel.SendMessageAsync(result.ToString()); + return Task.CompletedTask; } } } From a253b7ee75f7ec1a731de8a363f81f466b4952be Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 20 Oct 2018 10:07:10 -0400 Subject: [PATCH 13/26] docs: remove duplicated MessageReceived hook --- samples/03_sharded_client/Services/CommandHandlingService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/03_sharded_client/Services/CommandHandlingService.cs b/samples/03_sharded_client/Services/CommandHandlingService.cs index 08a3887f0..1230cbcff 100644 --- a/samples/03_sharded_client/Services/CommandHandlingService.cs +++ b/samples/03_sharded_client/Services/CommandHandlingService.cs @@ -28,7 +28,6 @@ namespace _03_sharded_client.Services public async Task InitializeAsync() { await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); - _discord.MessageReceived += MessageReceivedAsync; } public async Task MessageReceivedAsync(SocketMessage rawMessage) From 62607490952511ab5fc41965cea10f9879914c95 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 20 Oct 2018 10:52:26 -0400 Subject: [PATCH 14/26] fix: invoke CommandExecuted on async exception failures --- src/Discord.Net.Commands/Info/CommandInfo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index d27a1ed7b..87434546f 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -272,6 +272,10 @@ namespace Discord.Commands var wrappedEx = new CommandException(this, context, ex); await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); + + var result = ExecuteResult.FromError(CommandError.Exception, ex.Message); + await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); + if (Module.Service._throwOnError) { if (ex == originalEx) @@ -280,7 +284,7 @@ namespace Discord.Commands ExceptionDispatchInfo.Capture(ex).Throw(); } - return ExecuteResult.FromError(CommandError.Exception, ex.Message); + return result; } finally { From f549da50e0cb491350e2116cbb141c82c460a455 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 20 Oct 2018 11:01:17 -0400 Subject: [PATCH 15/26] fix: pass the entire Exception into ExecuteResult --- src/Discord.Net.Commands/Info/CommandInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 87434546f..a8aa3157c 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -273,7 +273,7 @@ namespace Discord.Commands var wrappedEx = new CommandException(this, context, ex); await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); - var result = ExecuteResult.FromError(CommandError.Exception, ex.Message); + var result = ExecuteResult.FromError(ex); await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); if (Module.Service._throwOnError) From 649a779c23737233933bac1b13a976e2b8a536f6 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 21 Oct 2018 00:45:05 +0800 Subject: [PATCH 16/26] feature: Add invite maxAge check before attempting to create the invite (#1140) * Add maxage check & improve maxage parameter check * Change InvalidOperation to ArgumentOoR * Remove HasValue check ...since it does it implicitly already. * Add parameter names & better wording for invite OoR * Move maxAge check to DiscordRestApiClient for consistency --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 ++ .../Entities/Channels/ChannelHelper.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 284a51756..7ed57ed6f 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -978,6 +978,8 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge)); Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses)); + Preconditions.AtMost(args.MaxAge, 86400, nameof(args.MaxAge), + "The maximum age of an invite must be less than or equal to a day (86400 seconds)."); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 65b4869b8..5c232f292 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -76,9 +76,13 @@ namespace Discord.Rest public static async Task CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) { - var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique }; - args.MaxAge = maxAge.GetValueOrDefault(0); - args.MaxUses = maxUses.GetValueOrDefault(0); + var args = new API.Rest.CreateChannelInviteParams + { + IsTemporary = isTemporary, + IsUnique = isUnique, + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0 + }; var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); return RestInviteMetadata.Create(client, null, channel, model); } From 637d9fc79404700dbe8a0c76d741f01b8986b422 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sat, 20 Oct 2018 14:40:13 -0400 Subject: [PATCH 17/26] Add SocketUser.MutualGuilds + various command ext. methods. (#1037) * Add SocketUser.MutualGuilds + various ext. methods. * Search through submodules for GetExecutableCommandAsync * Allow GetExecutableCommandsAsync(ModuleInfo) to recurse properly to all submodules. * Bump down lang. version & whitespace cleanup. * Change to use Task.WhenAll * Change to ICollection * Resolve build errors. --- .../Extensions/CommandServiceExtensions.cs | 42 +++++++++++++++++++ .../Extensions/EmbedBuilderExtensions.cs | 12 ++++++ .../Entities/Users/SocketUser.cs | 6 ++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs diff --git a/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs new file mode 100644 index 000000000..0a1c1646c --- /dev/null +++ b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + public static class CommandServiceExtensions + { + public static async Task> GetExecutableCommandsAsync(this ICollection commands, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + var tasks = commands.Select(async c => { var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); return new { Command = c, PreconditionResult = result }; }); + + var results = await Task.WhenAll(tasks); + + foreach (var result in results) + { + if (result.PreconditionResult.IsSuccess) + executableCommands.Add(result.Command); + } + + return executableCommands; + } + public static Task> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, IServiceProvider provider) + => GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider); + public static async Task> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider) + { + var executableCommands = new List(); + + executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + + var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); + var results = await Task.WhenAll(tasks); + + executableCommands.AddRange(results.SelectMany(c => c)); + + return executableCommands; + } + } +} diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 2d72dc985..60fdcfbee 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace Discord { @@ -65,5 +67,15 @@ namespace Discord return builder; } + + public static EmbedBuilder WithFields(this EmbedBuilder builder, IEnumerable fields) + { + foreach (var field in fields) + builder.AddField(field); + + return builder; + } + public static EmbedBuilder WithFields(this EmbedBuilder builder, params EmbedFieldBuilder[] fields) + => WithFields(builder, fields.AsEnumerable()); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 9a101cddb..f0dc92b1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,7 +1,9 @@ -using Discord.Rest; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; +using Discord.Rest; using Model = Discord.API.User; namespace Discord.WebSocket @@ -35,6 +37,8 @@ namespace Discord.WebSocket public IActivity Activity => Presence.Activity; /// public UserStatus Status => Presence.Status; + public IEnumerable MutualGuilds + => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)); internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) From 5b3eb70ffdd1d791265aa38bfb3b313cbefbb3cd Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 20 Oct 2018 15:00:08 -0400 Subject: [PATCH 18/26] meta: add changelog --- CHANGELOG.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0388cb239 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,76 @@ +# Changelog + +## [Unreleased] +### Added + +- #747: `CommandService` now has a `CommandExecuted` event (e991715) +- #765: Parameters may have a name specified via `NameAttribute` (9c81ab9) +- #773: Both socket clients inherit from `BaseSocketClient` (9b7afec) +- #785: Primitives now automatically load a NullableTypeReader (cb0ff78) +- #819: Support for Welcome Message channels (30e867a) +- #835: Emoji may now be managed from a bot (b4bf046) +- #843: Webhooks may now be managed from a bot (7b2ddd0) +- #863: An embed may be converted to an `EmbedBuilder` using the `.ToEmbedBuilder()` method (5218e6b) +- #877: Support for reading rich presences (34b4e5a) +- #888: Users may now opt-in to using a proxy (678a723) +- #906: API Analyzers to assist users when writing their bot (f69ef2a) +- #907: Full support for channel categories (030422f) +- #913: Animated emoji may be read and written (a19ff18) +- #915: Unused parameters may be discarded, rather than failing the command (5f46aef) +- #929: Standard EqualityComparers for use in LINQ operations with the library's entities (b5e7548) +- 'html' variant added to the `EmbedType` enum (42c879c) + +### Fixed +- #742: `DiscordShardedClient#GetGuildFor` will now direct null guilds to Shard 0 (d5e9d6f) +- #743: Various issues with permissions and inheritance of permissions (f996338) +- #755: `IRole.Mention` will correctly tag the @everyone role (6b5a6e7) +- #768: `CreateGuildAsync` will include the icon stream (865080a) +- #866: Revised permissions constants and behavior (dec7cb2) +- #872: Bulk message deletion should no longer fail for incomplete batch sizes (804d918) +- #923: A null value should properly reset a user's nickname (227f61a) +- #938: The reconnect handler should no longer deadlock during Discord outages (73ac9d7) +- Ignore messages with no ID in bulk delete (676be40) + +### Changed +- #731: `IUserMessage#GetReactionUsersAsync` now takes an `IEmote` instead of a `string` (5d7f2fc) +- #744: IAsyncEnumerable has been redesigned (5bbd9bb) +- #777: `IGuild#DefaultChannel` will now resolve the first accessible channel, per changes to Discord (1ffcd4b) +- #781: Attempting to add or remove a member's EveryoneRole will throw (506a6c9) +- #801: `EmbedBuilder` will no longer implicitly convert to `Embed`, you must build manually (94f7dd2) +- #804: Command-related tasks will have the 'async' suffix (14fbe40) +- #812: The WebSocket4Net provider has been bumped to version 0.15, allowing support for .NET Standard apps (e25054b) +- #829: DeleteMessagesAsync moved from IMessageChannel to ITextChannel (e00f17f) +- #853: WebSocket will now use `zlib-stream` compression (759db34) +- #874: The `ReadMessages` permission is moving to `ViewChannel` (edfbd05) +- #877: Refactored Games into Activities (34b4e5a) +- `IGuildChannel#Nsfw` moved to `ITextChannel`, now maps to the API property (608bc35) +- Preemptive ratelimits are now logged under verbose, rather than warning. (3c1e766) +- The default InviteAge when creating Invites is now 24 hours (9979a02) + + +### Removed +- #790: Redundant overloads for `AddField` removed from EmbedBuilder (479361b) +- #925: RPC is no longer being maintained nor packaged (b30af57) +- User logins (including selfbots) are no longer supported (fc5adca) + +### Misc +- This project is now licensed to the Discord.Net contributors (710e182) +- #786: Unit tests for the Color structure (22b969c) +- #828: We now include a contributing guide (cd82a0f) +- #876: We now include a standard editorconfig (5c8c784) + +## [1.0.2] - 2017-09-09 +### Fixed + +- Guilds utilizing Channel Categories will no longer crash bots on the `READY` event. + +## [1.0.1] - 2017-07-05 +### Fixed + +- #732: Fixed parameter preconditions not being loaded from class-based modules (b6dcc9e) +- #726: Fixed CalculateScore throwing an ArgumentException for missing parameters (7597cf5) +- EmbedBuilder URI validation should no longer throw NullReferenceExceptions in certain edge cases (d89804d) +- Fixed module auto-detection for nested modules (d2afb06) + +### Changed +- ShardedCommandContext now inherits from SocketCommandContext (8cd99be) From 8fa70bf6a65dde24f6ab85dd279f3f10a93c8c77 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Mon, 22 Oct 2018 05:26:39 +0800 Subject: [PATCH 19/26] lint: Initial clean-up (#1181) --- .../Extensions/CommandServiceExtensions.cs | 10 +++++++--- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs index 0a1c1646c..675a9073d 100644 --- a/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/CommandServiceExtensions.cs @@ -11,9 +11,13 @@ namespace Discord.Commands { var executableCommands = new List(); - var tasks = commands.Select(async c => { var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); return new { Command = c, PreconditionResult = result }; }); + var tasks = commands.Select(async c => + { + var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false); + return new { Command = c, PreconditionResult = result }; + }); - var results = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); foreach (var result in results) { @@ -32,7 +36,7 @@ namespace Discord.Commands executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false)); - var results = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); executableCommands.AddRange(results.SelectMany(c => c)); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index f0dc92b1c..41c463a89 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -37,8 +38,8 @@ namespace Discord.WebSocket public IActivity Activity => Presence.Activity; /// public UserStatus Status => Presence.Status; - public IEnumerable MutualGuilds - => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)); + public IReadOnlyCollection MutualGuilds + => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) From 8e6e0e9ad2fe535b28ab5ed0f8e5b9e127cde092 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sun, 21 Oct 2018 14:27:02 -0700 Subject: [PATCH 20/26] lint: Remove public setters for MessageApplication, MessageActivity properties (#1182) This fixes an error that was introduced in d30d12246d5173321c2556cf65d3c69da230cb8c (thanks Still for noticing!) Changes the public property setters of the MessageActivity and MessageApplication entities to be internal, since they cannot be modified by the user in the API. --- .../Entities/Messages/MessageActivity.cs | 4 ++-- .../Entities/Messages/MessageApplication.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs index d19e6a8e9..b539b8f9b 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageActivity.cs @@ -13,11 +13,11 @@ namespace Discord /// /// Gets the type of activity of this message. /// - public MessageActivityType Type { get; set; } + public MessageActivityType Type { get; internal set; } /// /// Gets the party ID of this activity, if any. /// - public string PartyId { get; set; } + public string PartyId { get; internal set; } private string DebuggerDisplay => $"{Type}{(string.IsNullOrWhiteSpace(PartyId) ? "" : $" {PartyId}")}"; diff --git a/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs index 960d1700f..05616cb59 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageApplication.cs @@ -13,19 +13,19 @@ namespace Discord /// /// Gets the snowflake ID of the application. /// - public ulong Id { get; set; } + public ulong Id { get; internal set; } /// /// Gets the ID of the embed's image asset. /// - public string CoverImage { get; set; } + public string CoverImage { get; internal set; } /// /// Gets the application's description. /// - public string Description { get; set; } + public string Description { get; internal set; } /// /// Gets the ID of the application's icon. /// - public string Icon { get; set; } + public string Icon { get; internal set; } /// /// Gets the Url of the application's icon. /// @@ -34,7 +34,7 @@ namespace Discord /// /// Gets the name of the application. /// - public string Name { get; set; } + public string Name { get; internal set; } private string DebuggerDisplay => $"{Name} ({Id}): {Description}"; public override string ToString() From c1d51522122688fdc598c5225a0cd08b0f47017b Mon Sep 17 00:00:00 2001 From: OatmealDome Date: Sun, 4 Nov 2018 17:43:36 -0500 Subject: [PATCH 21/26] fix: UploadWebhookFileParams: Move most things into a json_payload (#1186) --- .../API/Rest/UploadWebhookFileParams.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 6d6eb29b2..479a7857a 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -1,12 +1,17 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using System.Collections.Generic; using System.IO; +using System.Text; +using Discord.Net.Converters; using Discord.Net.Rest; +using Newtonsoft.Json; namespace Discord.API.Rest { internal class UploadWebhookFileParams { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + public Stream File { get; } public Optional Filename { get; set; } @@ -27,18 +32,27 @@ namespace Discord.API.Rest var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + var payload = new Dictionary(); if (Content.IsSpecified) - d["content"] = Content.Value; + payload["content"] = Content.Value; if (IsTTS.IsSpecified) - d["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) - d["nonce"] = Nonce.Value; + payload["nonce"] = Nonce.Value; if (Username.IsSpecified) - d["username"] = Username.Value; + payload["username"] = Username.Value; if (AvatarUrl.IsSpecified) - d["avatar_url"] = AvatarUrl.Value; + payload["avatar_url"] = AvatarUrl.Value; if (Embeds.IsSpecified) - d["embeds"] = Embeds.Value; + payload["embeds"] = Embeds.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + return d; } } From 7dd2268982c4f269c1e361c7745d22139fc7f2d7 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Tue, 6 Nov 2018 07:32:30 +0800 Subject: [PATCH 22/26] lint: Discord Namespace Clean-up (#1185) * Remove RpcException * Move RestConnection to Discord.Net.Rest * Remove RpcVirtualMessageChannel * Move REST objects to Discord.Net.Rest * Fix Discord.Rest namespace --- src/Discord.Net.Core/Net/RpcException.cs | 17 --- .../Channels/RpcVirtualMessageChannel.cs | 110 ------------------ .../Entities/Guilds/RestGuildEmbed.cs | 2 +- .../Entities/Guilds/RestVoiceRegion.cs | 2 +- .../Entities/Users/RestConnection.cs | 2 +- 5 files changed, 3 insertions(+), 130 deletions(-) delete mode 100644 src/Discord.Net.Core/Net/RpcException.cs delete mode 100644 src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs diff --git a/src/Discord.Net.Core/Net/RpcException.cs b/src/Discord.Net.Core/Net/RpcException.cs deleted file mode 100644 index 195fad73f..000000000 --- a/src/Discord.Net.Core/Net/RpcException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord -{ - public class RpcException : Exception - { - public int ErrorCode { get; } - public string Reason { get; } - - public RpcException(int errorCode, string reason = null) - : base($"The server sent error {errorCode}{(reason != null ? $": \"{reason}\"" : "")}") - { - ErrorCode = errorCode; - Reason = reason; - } - } -} diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs deleted file mode 100644 index ce37af6b4..000000000 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - internal class RestVirtualMessageChannel : RestEntity, IMessageChannel - { - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - public string Mention => MentionUtils.MentionChannel(Id); - - internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) - : base(discord, id) - { - } - internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulong id) - { - return new RestVirtualMessageChannel(discord, id); - } - - public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, options); - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); - public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) - - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); - - /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); - /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) - => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); - - /// - public Task TriggerTypingAsync(RequestOptions options = null) - => ChannelHelper.TriggerTypingAsync(this, Discord, options); - /// - public IDisposable EnterTypingState(RequestOptions options = null) - => ChannelHelper.EnterTypingState(this, Discord, options); - - private string DebuggerDisplay => $"({Id}, Text)"; - - //IMessageChannel - async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options).ConfigureAwait(false); - else - return null; - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessageId, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return GetMessagesAsync(fromMessage, dir, limit, options); - else - return AsyncEnumerable.Empty>(); - } - async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - - //IChannel - string IChannel.Name => - throw new NotSupportedException(); - - 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/Guilds/RestGuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs index 00f3fae69..41c76eb06 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Model = Discord.API.GuildEmbed; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct RestGuildEmbed diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs index 7a03b68d4..a363f051b 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestVoiceRegion.cs @@ -2,7 +2,7 @@ using Discord.Rest; using System.Diagnostics; using Model = Discord.API.VoiceRegion; -namespace Discord +namespace Discord.Rest { /// /// Represents a REST-based voice region. diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index 0c91493d2..1afb813c0 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Model = Discord.API.Connection; -namespace Discord +namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestConnection : IConnection From 8ef5f8120ffdaaa0669b8ff6d9c8dc601ac78439 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Mon, 5 Nov 2018 18:34:09 -0500 Subject: [PATCH 23/26] feature: add Add Guild Member endpoint (#1183) * Add AddGuildMember Oauth endpoint support * Concat RoleIds if already exists. * Use local ids variable. --- .../Entities/Guilds/IGuild.cs | 12 ++++ .../Entities/Users/AddGuildUserProperties.cs | 62 +++++++++++++++++++ .../API/Rest/AddGuildMemberParams.cs | 19 ++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 19 ++++++ .../Entities/Guilds/GuildHelper.cs | 28 +++++++++ .../Entities/Guilds/RestGuild.cs | 8 +++ .../Entities/Guilds/SocketGuild.cs | 8 +++ 7 files changed, 156 insertions(+) create mode 100644 src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs create mode 100644 src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index c321cd2e3..a7206bd59 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -535,6 +535,18 @@ namespace Discord /// Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + /// + /// Adds a user to this guild. + /// + /// + /// This method requires you have an OAuth2 access token for the user, requested with the guilds.join scope, and that the bot have the MANAGE_INVITES permission in the guild. + /// + /// The snowflake identifier of the user. + /// The OAuth2 access token for the user, requested with the guilds.join scope. + /// The delegate containing the properties to be applied to the user upon being added to the guild. + /// The options to be used when sending the request. + /// A guild user associated with the specified ; null if the user is already in the guild. + Task AddGuildUserAsync(ulong userId, string accessToken, Action func = null, RequestOptions options = null); /// /// Gets a collection of all users in this guild. /// diff --git a/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs new file mode 100644 index 000000000..e380d9027 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/AddGuildUserProperties.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Properties that are used to add a new to the guild with the following parameters. + /// + /// + public class AddGuildUserProperties + { + /// + /// Gets or sets the user's nickname. + /// + /// + /// To clear the user's nickname, this value can be set to null or + /// . + /// + public Optional Nickname { get; set; } + /// + /// Gets or sets whether the user should be muted in a voice channel. + /// + /// + /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// + public Optional Mute { get; set; } + /// + /// Gets or sets whether the user should be deafened in a voice channel. + /// + /// + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// + public Optional Deaf { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> Roles { get; set; } + /// + /// Gets or sets the roles the user should have. + /// + /// + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// + /// + public Optional> RoleIds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs new file mode 100644 index 000000000..ef6229edb --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/AddGuildMemberParams.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class AddGuildMemberParams + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("nick")] + public Optional Nickname { get; set; } + [JsonProperty("roles")] + public Optional RoleIds { get; set; } + [JsonProperty("mute")] + public Optional IsMuted { get; set; } + [JsonProperty("deaf")] + public Optional IsDeafened { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 7ed57ed6f..57d7c718a 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -994,6 +994,25 @@ namespace Discord.API } //Guild Members + public async Task AddGuildMemberAsync(ulong guildId, ulong userId, AddGuildMemberParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotNullOrWhitespace(args.AccessToken, nameof(args.AccessToken)); + + if (args.RoleIds.IsSpecified) + { + foreach (var roleId in args.RoleIds.Value) + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + } + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PUT", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options); + } public async Task GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7b77dafc7..c31fa89f2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -257,6 +257,34 @@ namespace Discord.Rest } //Users + public static async Task AddGuildUserAsync(IGuild guild, BaseDiscordClient client, ulong userId, string accessToken, + Action func, RequestOptions options) + { + var args = new AddGuildUserProperties(); + func?.Invoke(args); + + if (args.Roles.IsSpecified) + { + var ids = args.Roles.Value.Select(r => r.Id); + + if (args.RoleIds.IsSpecified) + args.RoleIds.Value.Concat(ids); + else + args.RoleIds = Optional.Create(ids); + } + var apiArgs = new AddGuildMemberParams + { + AccessToken = accessToken, + Nickname = args.Nickname, + IsDeafened = args.Deaf, + IsMuted = args.Mute, + RoleIds = args.RoleIds.IsSpecified ? args.RoleIds.Value.Distinct().ToArray() : Optional.Create() + }; + + var model = await client.ApiClient.AddGuildMemberAsync(guild.Id, userId, apiArgs, options); + + return model is null ? null : RestGuildUser.Create(client, guild, model); + } public static async Task GetUserAsync(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 8cd81f218..bd70bf96b 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -537,6 +537,10 @@ namespace Discord.Rest public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) => GuildHelper.GetUsersAsync(this, Discord, null, null, options); + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + /// /// Gets a user from this guild. /// @@ -800,6 +804,10 @@ namespace Discord.Rest async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); + /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index ce8cd08de..412f3acff 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -669,6 +669,10 @@ namespace Discord.WebSocket } //Users + /// + public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) + => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); + /// /// Gets a user from this guild. /// @@ -1096,6 +1100,10 @@ namespace Discord.WebSocket /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Users); + + /// + async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) + => await AddGuildUserAsync(userId, accessToken, func, options); /// Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); From 419c0a5b5312391903c0f72344609c4dae486a07 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Wed, 7 Nov 2018 00:36:44 +0100 Subject: [PATCH 24/26] feature: Add a way to invoke a command specifying optional values by name (#1123) * Add NamedArgumentTypeAttribute * Add NamedArgumentTypeReader * Fix superflous empty line. * Fix logic for quoted arguments * Throw an exception with a tailored message. * Add a catch to wrap parsing/input errors * Trim potential excess whitespace * Fix an off-by-one * Support to read an IEnumerable property * Add a doc * Add assertion for the collection test --- .../Attributes/NamedArgumentTypeAttribute.cs | 11 + .../Attributes/OverrideTypeReaderAttribute.cs | 5 +- .../Builders/ModuleClassBuilder.cs | 2 +- .../Builders/ParameterBuilder.cs | 29 ++- .../Readers/NamedArgumentTypeReader.cs | 191 ++++++++++++++++++ .../Discord.Net.Tests.csproj | 7 +- test/Discord.Net.Tests/Tests.TypeReaders.cs | 133 ++++++++++++ 7 files changed, 369 insertions(+), 9 deletions(-) create mode 100644 src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs create mode 100644 src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs create mode 100644 test/Discord.Net.Tests/Tests.TypeReaders.cs diff --git a/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs new file mode 100644 index 000000000..a43286110 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + /// + /// Instructs the command system to treat command paramters of this type + /// as a collection of named arguments matching to its properties. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class NamedArgumentTypeAttribute : Attribute { } +} diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 85f5df10e..a44dcb6e4 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -1,5 +1,4 @@ using System; - using System.Reflection; namespace Discord.Commands @@ -27,8 +26,8 @@ namespace Discord.Commands /// => ReplyAsync(time); /// /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public class OverrideTypeReaderAttribute : Attribute + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 3b71c87b0..aec8dcbe3 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -280,7 +280,7 @@ namespace Discord.Commands } } - private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) + internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) { var readers = service.GetTypeReaders(paramType); TypeReader reader = null; diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 8a59c247c..4ad5bfac0 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -56,11 +56,36 @@ namespace Discord.Commands.Builders private TypeReader GetReader(Type type) { - var readers = Command.Module.Service.GetTypeReaders(type); + var commands = Command.Module.Service; + if (type.GetTypeInfo().GetCustomAttribute() != null) + { + IsRemainder = true; + var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value; + if (reader == null) + { + Type readerType; + try + { + readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type }); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex); + } + + reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands }); + commands.AddTypeReader(type, reader); + } + + return reader; + } + + + var readers = commands.GetTypeReaders(type); if (readers != null) return readers.FirstOrDefault().Value; else - return Command.Module.Service.GetDefaultTypeReader(type); + return commands.GetDefaultTypeReader(type); } public ParameterBuilder WithSummary(string summary) diff --git a/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs new file mode 100644 index 000000000..01559293f --- /dev/null +++ b/src/Discord.Net.Commands/Readers/NamedArgumentTypeReader.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + internal sealed class NamedArgumentTypeReader : TypeReader + where T : class, new() + { + private static readonly IReadOnlyDictionary _tProps = typeof(T).GetTypeInfo().DeclaredProperties + .Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic) + .ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + + private readonly CommandService _commands; + + public NamedArgumentTypeReader(CommandService commands) + { + _commands = commands; + } + + public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var result = new T(); + var state = ReadState.LookingForParameter; + int beginRead = 0, currentRead = 0; + + while (state != ReadState.End) + { + try + { + var prop = Read(out var arg); + var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false); + if (propVal != null) + prop.SetMethod.Invoke(result, new[] { propVal }); + else + return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'."); + } + catch (Exception ex) + { + //TODO: use the Exception overload after a rebase on latest + return TypeReaderResult.FromError(CommandError.Exception, ex.Message); + } + } + + return TypeReaderResult.FromSuccess(result); + + PropertyInfo Read(out string arg) + { + string currentParam = null; + char match = '\0'; + + for (; currentRead < input.Length; currentRead++) + { + var currentChar = input[currentRead]; + switch (state) + { + case ReadState.LookingForParameter: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = ReadState.InParameter; + } + break; + case ReadState.InParameter: + if (currentChar != ':') + continue; + else + { + currentParam = input.Substring(beginRead, currentRead - beginRead); + state = ReadState.LookingForArgument; + } + break; + case ReadState.LookingForArgument: + if (Char.IsWhiteSpace(currentChar)) + continue; + else + { + beginRead = currentRead; + state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match)) + ? ReadState.InQuotedArgument + : ReadState.InArgument; + } + break; + case ReadState.InArgument: + if (!Char.IsWhiteSpace(currentChar)) + continue; + else + return GetPropAndValue(out arg); + case ReadState.InQuotedArgument: + if (currentChar != match) + continue; + else + return GetPropAndValue(out arg); + } + } + + if (currentParam == null) + throw new InvalidOperationException("No parameter name was read."); + + return GetPropAndValue(out arg); + + PropertyInfo GetPropAndValue(out string argv) + { + bool quoted = state == ReadState.InQuotedArgument; + state = (currentRead == (quoted ? input.Length - 1 : input.Length)) + ? ReadState.End + : ReadState.LookingForParameter; + + if (quoted) + { + argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim(); + currentRead++; + } + else + argv = input.Substring(beginRead, currentRead - beginRead); + + return _tProps[currentParam]; + } + } + + async Task ReadArgumentAsync(PropertyInfo prop, string arg) + { + var elemType = prop.PropertyType; + bool isCollection = false; + if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elemType = prop.PropertyType.GenericTypeArguments[0]; + isCollection = true; + } + + var overridden = prop.GetCustomAttribute(); + var reader = (overridden != null) + ? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services) + : (_commands.GetDefaultTypeReader(elemType) + ?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value); + + if (reader != null) + { + if (isCollection) + { + var method = _readMultipleMethod.MakeGenericMethod(elemType); + var task = (Task)method.Invoke(null, new object[] { reader, context, arg.Split(','), services }); + return await task.ConfigureAwait(false); + } + else + return await ReadSingle(reader, context, arg, services).ConfigureAwait(false); + } + return null; + } + } + + private static async Task ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services) + { + var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false); + return (readResult.IsSuccess) + ? readResult.BestMatch + : null; + } + private static async Task ReadMultiple(TypeReader reader, ICommandContext context, IEnumerable args, IServiceProvider services) + { + var objs = new List(); + foreach (var arg in args) + { + var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false); + if (read != null) + objs.Add((TObj)read); + } + return objs.ToImmutableArray(); + } + private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader) + .GetTypeInfo() + .DeclaredMethods + .Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple)); + + private enum ReadState + { + LookingForParameter, + InParameter, + LookingForArgument, + InArgument, + InQuotedArgument, + End + } + } +} diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 60491a96f..0ee6f7e59 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -3,6 +3,7 @@ Exe Discord netcoreapp1.1 + portable $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 @@ -23,8 +24,8 @@ - - - + + + diff --git a/test/Discord.Net.Tests/Tests.TypeReaders.cs b/test/Discord.Net.Tests/Tests.TypeReaders.cs new file mode 100644 index 000000000..91514bfae --- /dev/null +++ b/test/Discord.Net.Tests/Tests.TypeReaders.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord.Commands; +using Xunit; + +namespace Discord +{ + public sealed class TypeReaderTests + { + [Fact] + public async Task TestNamedArgumentReader() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "bar: hello foo: 42"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + + [Fact] + public async Task TestQuotedArgumentValue() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foo: 42 bar: 《hello》"); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: 42, actual: m.Foo); + Assert.Equal(expected: "hello", actual: m.Bar); + } + + [Fact] + public async Task TestNonPatternInput() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "foobar"); + Assert.False(result.IsSuccess); + Assert.Equal(expected: CommandError.Exception, actual: result.Error); + } + + [Fact] + public async Task TestMultiple() + { + var commands = new CommandService(); + var module = await commands.AddModuleAsync(null); + + Assert.NotNull(module); + Assert.NotEmpty(module.Commands); + + var cmd = module.Commands[0]; + Assert.NotNull(cmd); + Assert.NotEmpty(cmd.Parameters); + + var param = cmd.Parameters[0]; + Assert.NotNull(param); + Assert.True(param.IsRemainder); + + var result = await param.ParseAsync(null, "manyints: \"1, 2, 3, 4, 5, 6, 7\""); + Assert.True(result.IsSuccess); + + var m = result.BestMatch as ArgumentType; + Assert.NotNull(m); + Assert.Equal(expected: new int[] { 1, 2, 3, 4, 5, 6, 7 }, actual: m.ManyInts); + } + } + + [NamedArgumentType] + public sealed class ArgumentType + { + public int Foo { get; set; } + + [OverrideTypeReader(typeof(CustomTypeReader))] + public string Bar { get; set; } + + public IEnumerable ManyInts { get; set; } + } + + public sealed class CustomTypeReader : TypeReader + { + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + => Task.FromResult(TypeReaderResult.FromSuccess(input)); + } + + public sealed class TestModule : ModuleBase + { + [Command("test")] + public Task TestCommand(ArgumentType arg) => Task.Delay(0); + } +} From f9c95bc4610899fcd286b7d6e930c48cd5c9e9a5 Mon Sep 17 00:00:00 2001 From: Casino Boyale Date: Thu, 8 Nov 2018 15:58:21 +0000 Subject: [PATCH 25/26] Got rid of use of undefined variables --- docs/guides/voice/samples/joining_audio.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs index 3abbfb632..8803d3596 100644 --- a/docs/guides/voice/samples/joining_audio.cs +++ b/docs/guides/voice/samples/joining_audio.cs @@ -3,8 +3,8 @@ public async Task JoinChannel(IVoiceChannel channel = null) { // Get the audio channel - channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; - if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } + channel = channel ?? (Context.User as IGuildUser)?.VoiceChannel; + if (channel == null) { await Context.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. var audioClient = await channel.ConnectAsync(); From 10233f3a9a67a6e2cb63e98060c71662dc60c45a Mon Sep 17 00:00:00 2001 From: Casino Boyale Date: Tue, 20 Nov 2018 19:16:27 +0000 Subject: [PATCH 26/26] fix: Fixed CommandExecuted firing twice for failed RuntimeResults (#1192) * Fixed CommandExecuted firing twice for failed RuntimeResults * Changed to just checking the result type * Amendments --- src/Discord.Net.Commands/CommandService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 432b75f27..d5b3f9ff4 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -603,7 +603,7 @@ namespace Discord.Commands //If we get this far, at least one parse was successful. Execute the most likely overload. var chosenOverload = successfulParses[0]; var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); - if (!result.IsSuccess) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + if (!result.IsSuccess && !(result is RuntimeResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); return result; }