diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 84ee6e5a1..807381d31 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,3 @@ +github: quinchs open_collective: discordnet +custom: https://paypal.me/quinchs diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index e2c154130..29759facf 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -38,7 +38,7 @@ body: id: description attributes: label: Description - description: A brief explination of the bug. + description: A brief explanation of the bug. placeholder: When I start a DiscordSocketClient without stopping it, the gateway thread gets blocked. validations: required: true @@ -62,7 +62,7 @@ body: id: logs attributes: label: Logs - description: Add applicable logs and/or a stacktrace here. + description: Add applicable logs and/or a stack trace here. validations: required: true - type: textarea diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4de065c..023400c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [3.6.1] - 2022-04-30 +### Added +- #2272 add 50080 Error code (503e720) + +### Fixed +- #2267 Permissions v2 Invalid Operation Exception (a8f6075) +- #2271 null user on interaction without bot scope (f2bb55e) +- #2274 Implement fix for Custom Id Segments NRE (0d74c5c) + +### Misc +- 3.6.0 (27226f0) + + +## [3.6.0] - 2022-04-28 +### Added +- #2136 Passing CustomId matches into contexts (4ce1801) +- #2222 V2 Permissions (d98b3cc) + +### Fixed +- #2260 Guarding against empty descriptions in `SlashCommandBuilder`/`SlashCommandOptionBuilder` (0554ac2) +- #2248 Fix SocketGuild not returning the AudioClient (daba58c) +- #2254 Fix browser property (275b833) + ## [3.5.0] - 2022-04-05 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index e50e6eceb..adb0a338c 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.5.0 + 3.6.1 latest Discord.Net Contributors discord;discordapp diff --git a/README.md b/README.md index 541948f4b..e85216dbf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# Discord.Net

Logo @@ -18,7 +17,7 @@ Discord

-Discord NET is an unofficial .NET API Wrapper for the Discord client (https://discord.com). +Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com). ## Documentation diff --git a/docs/docfx.json b/docs/docfx.json index 2a4ee2867..105aa0493 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.5.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.6.1", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index e1af20d30..a5b0dbbd4 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -202,7 +202,7 @@ online in Discord. To create commands for your bot, you may choose from a variety of command processors available. Throughout the guides, we will be using -the one that Discord.Net ships with. @Guides.Commands.Intro will +the one that Discord.Net ships with. @Guides.TextCommands.Intro will guide you through how to setup a program that is ready for [CommandService]. diff --git a/docs/guides/int_basics/application-commands/slash-commands/parameters.md b/docs/guides/int_basics/application-commands/slash-commands/parameters.md index 6afd83729..4f3cd2e8c 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/parameters.md +++ b/docs/guides/int_basics/application-commands/slash-commands/parameters.md @@ -15,9 +15,10 @@ Slash commands can have a bunch of parameters, each their own type. Let's first | Integer | A number. | | Boolean | True or False. | | User | A user | -| Channel | A channel, this includes voice text and categories | | Role | A role. | +| Channel | A channel, this includes voice text and categories | | Mentionable | A role or a user. | +| File | A file | Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field: | Name | C# Type | @@ -31,6 +32,7 @@ Each one of the parameter types has its own DNET type in the `SocketSlashCommand | Role | `SocketRole` | | Channel | `SocketChannel` | | Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` | +| File | `IAttachment` | Let's start by making a command that takes in a user and lists their roles. diff --git a/docs/guides/int_framework/autocompletion.md b/docs/guides/int_framework/autocompletion.md index 834db2b4f..27da54e36 100644 --- a/docs/guides/int_framework/autocompletion.md +++ b/docs/guides/int_framework/autocompletion.md @@ -18,6 +18,8 @@ AutocompleteHandlers raise the `AutocompleteHandlerExecuted` event on execution. A valid AutocompleteHandlers must inherit [AutocompleteHandler] base type and implement all of its abstract methods. +[!code-csharp[Autocomplete Command Example](samples/autocompletion/autocomplete-example.cs)] + ### GenerateSuggestionsAsync() The Interactions Service uses this method to generate a response of an Autocomplete Interaction. diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index abea2a735..54e9086a1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types: - Implementations of [IChannel] - Implementations of [IRole] - Implementations of [IMentionable] +- Implementations of [IAttachment] - `string` - `float`, `double`, `decimal` - `bool` @@ -158,6 +159,14 @@ Interaction service complex parameter constructors are prioritized in the follow 2. Constuctor tagged with `[ComplexParameterCtor]`. 3. Type's only public constuctor. +#### DM Permissions + +You can use the [EnabledInDmAttribute] to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands. + +#### Default Member Permissions + +[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups. + ## User Commands A valid User Command must have the following structure: @@ -282,6 +291,8 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can > Although creating nested module stuctures are allowed, > you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy. +[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] + ## Executing Commands Any of the following socket events can be used to execute commands: diff --git a/docs/guides/int_framework/permissions.md b/docs/guides/int_framework/permissions.md new file mode 100644 index 000000000..e35bb162d --- /dev/null +++ b/docs/guides/int_framework/permissions.md @@ -0,0 +1,59 @@ +--- +uid: Guides.IntFw.Perms +title: How to handle permissions. +--- + +# Permissions + +This page covers everything to know about setting up permissions for Slash & context commands. + +Application command (Slash, User & Message) permissions are set up at creation. +When you add your commands to a guild or globally, the permissions will be set up from the attributes you defined. + +Commands that are added will only show up for members that meet the required permissions. +There is no further internal handling, as Discord deals with this on its own. + +> [!WARNING] +> Permissions can only be configured at top level commands. Not in subcommands. + +## Disallowing commands in DM + +Commands can be blocked from being executed in DM if a guild is required to execute them in as followed: + +[!code-csharp[no-DM permission](samples/permissions/guild-only.cs)] + +> [!TIP] +> This attribute only works on global-level commands. Commands that are registered in guilds alone do not have a need for it. + +## Server permissions + +As previously shown, a command like ban can be blocked from being executed inside DMs, +as there are no members to ban inside of a DM. However, for a command like this, +we'll also want to make block it from being used by members that do not have the [permissions]. +To do this, we can use the `DefaultMemberPermissions` attribute: + +[!code-csharp[Server permissions](samples/permissions/guild-perms.cs)] + +### Stacking permissions + +If you want a user to have multiple [permissions] in order to execute a command, you can use the `|` operator, just like with setting up intents: + +[!code-csharp[Permission stacking](samples/permissions/perm-stacking.cs)] + +### Nesting permissions + +Alternatively, permissions can also be nested. +It will look for all uses of `DefaultMemberPermissions` up until the highest level class. +The `EnabledInDm` attribute can be defined at top level as well, +and will be set up for all of the commands & nested modules inside this class. + +[!code-csharp[Permission stacking](samples/permissions/perm-nesting.cs)] + +The amount of nesting you can do is realistically endless. + +> [!NOTE] +> If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. +> As mentioned before, subcommands cannot have seperate permissions from the top level command. + +[permissions]: xref:Discord.GuildPermission + diff --git a/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs new file mode 100644 index 000000000..30c0697e1 --- /dev/null +++ b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs @@ -0,0 +1,20 @@ +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete(typeof(ExampleAutocompleteHandler))] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); + +public class ExampleAutocompleteHandler : AutocompleteHandler +{ + public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + { + // Create a collection with suggestions for autocomplete + IEnumerable results = new[] + { + new AutocompleteResult("Name1", "value111"), + new AutocompleteResult("Name2", "value2") + }; + + // max - 25 suggestions at a time (API limit) + return AutocompletionResult.FromSuccess(results.Take(25)); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/autocomplete.cs b/docs/guides/int_framework/samples/intro/autocomplete.cs index f93c56eaa..11de489f1 100644 --- a/docs/guides/int_framework/samples/intro/autocomplete.cs +++ b/docs/guides/int_framework/samples/intro/autocomplete.cs @@ -1,9 +1,21 @@ [AutocompleteCommand("parameter_name", "command_name")] public async Task Autocomplete() { - IEnumerable results; + string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString(); - ... + IEnumerable results = new[] + { + new AutocompleteResult("foo", "foo_value"), + new AutocompleteResult("bar", "bar_value"), + new AutocompleteResult("baz", "baz_value"), + }.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching - await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results); + + // max - 25 suggestions at a time + await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25)); } + +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs new file mode 100644 index 000000000..f0d992aff --- /dev/null +++ b/docs/guides/int_framework/samples/intro/groupmodule.cs @@ -0,0 +1,21 @@ +// You can put commands in groups +[Group("group-name", "Group description")] +public class CommandGroupModule : InteractionModuleBase +{ + // This command will look like + // group-name ping + [SlashCommand("ping", "Get a pong")] + public async Task PongSubcommand() + => await RespondAsync("Pong!"); + + // And even in sub-command groups + [Group("subcommand-group-name", "Subcommand group description")] + public class SubСommandGroupModule : InteractionModuleBase + { + // This command will look like + // group-name subcommand-group-name echo + [SlashCommand("echo", "Echo an input")] + public async Task EchoSubcommand(string input) + => await RespondAsync(input); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/permissions/guild-only.cs b/docs/guides/int_framework/samples/permissions/guild-only.cs new file mode 100644 index 000000000..2e907e2d3 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-only.cs @@ -0,0 +1,6 @@ +[EnabledInDm(false)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/guild-perms.cs b/docs/guides/int_framework/samples/permissions/guild-perms.cs new file mode 100644 index 000000000..2853f23e7 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-perms.cs @@ -0,0 +1,7 @@ +[EnabledInDm(false)] +[DefaultMemberPermissions(GuildPermission.BanMembers)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/perm-nesting.cs b/docs/guides/int_framework/samples/permissions/perm-nesting.cs new file mode 100644 index 000000000..8913b1ac1 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-nesting.cs @@ -0,0 +1,16 @@ +[EnabledInDm(true)] +[DefaultMemberPermissions(GuildPermission.ViewChannels)] +public class Module : InteractionModuleBase +{ + [DefaultMemberPermissions(GuildPermission.SendMessages)] + public class NestedModule : InteractionModuleBase + { + // While looking for more permissions, it has found 'ViewChannels' and 'SendMessages'. The result of this lookup will be: + // ViewChannels + SendMessages + ManageMessages. + // If these together are not found for target user, the command will not show up for them. + [DefaultMemberPermissions(GuildPermission.ManageMessages)] + [SlashCommand("ping", "Pong!")] + public async Task Ping() + => await RespondAsync("pong"); + } +} diff --git a/docs/guides/int_framework/samples/permissions/perm-stacking.cs b/docs/guides/int_framework/samples/permissions/perm-stacking.cs new file mode 100644 index 000000000..92cc51477 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-stacking.cs @@ -0,0 +1,4 @@ +[DefaultMemberPermissions(GuildPermission.SendMessages | GuildPermission.ViewChannels)] +[SlashCommand("ping", "Pong!")] +public async Task Ping() + => await RespondAsync("pong"); diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs index b4870cfd1..0f7c11daf 100644 --- a/docs/guides/other_libs/samples/ModifyLogMethod.cs +++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs @@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message) LogSeverity.Error => LogEventLevel.Error, LogSeverity.Warning => LogEventLevel.Warning, LogSeverity.Info => LogEventLevel.Information, - LogSeverity.Verbose => LogEventLevel.Debug, - LogSeverity.Debug => LogEventLevel.Verbose, + LogSeverity.Verbose => LogEventLevel.Verbose, + LogSeverity.Debug => LogEventLevel.Debug, _ => LogEventLevel.Information }; Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index af0a8e2b4..f122ea6ba 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -57,6 +57,8 @@ topicUid: Guides.IntFw.DI - name: Post-execution Handling topicUid: Guides.IntFw.PostExecution + - name: Permissions + topicUid: Guides.IntFw.Perms - name: Slash Command Basics items: - name: Introduction diff --git a/samples/BasicBot/_BasicBot.csproj b/samples/BasicBot/_BasicBot.csproj index 6e1a6365f..e6245d340 100644 --- a/samples/BasicBot/_BasicBot.csproj +++ b/samples/BasicBot/_BasicBot.csproj @@ -1,12 +1,12 @@ - + Exe - net5.0 + net6.0 - + diff --git a/samples/InteractionFramework/Modules/ExampleModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs index 1c0a6c8a2..21064bbe3 100644 --- a/samples/InteractionFramework/Modules/ExampleModule.cs +++ b/samples/InteractionFramework/Modules/ExampleModule.cs @@ -14,7 +14,7 @@ namespace InteractionFramework.Modules private InteractionHandler _handler; - // Constructor injection is also a valid way to access the dependecies + // Constructor injection is also a valid way to access the dependencies public ExampleModule(InteractionHandler handler) { _handler = handler; diff --git a/samples/InteractionFramework/_InteractionFramework.csproj b/samples/InteractionFramework/_InteractionFramework.csproj index f11c2bd3d..8892a65b7 100644 --- a/samples/InteractionFramework/_InteractionFramework.csproj +++ b/samples/InteractionFramework/_InteractionFramework.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 InteractionFramework @@ -13,13 +13,7 @@ - - - - - - - + diff --git a/samples/MediatRSample/MediatRSample.sln b/samples/MediatRSample/MediatRSample.sln deleted file mode 100644 index d0599ae26..000000000 --- a/samples/MediatRSample/MediatRSample.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/samples/ShardedClient/_ShardedClient.csproj b/samples/ShardedClient/_ShardedClient.csproj index 69576ea27..68a43c7cd 100644 --- a/samples/ShardedClient/_ShardedClient.csproj +++ b/samples/ShardedClient/_ShardedClient.csproj @@ -2,18 +2,13 @@ Exe - net5.0 + net6.0 ShardedClient - - - - - - +
diff --git a/samples/TextCommandFramework/_TextCommandFramework.csproj b/samples/TextCommandFramework/_TextCommandFramework.csproj index ee64205f5..6e00625e8 100644 --- a/samples/TextCommandFramework/_TextCommandFramework.csproj +++ b/samples/TextCommandFramework/_TextCommandFramework.csproj @@ -2,17 +2,14 @@ Exe - net5.0 + net6.0 TextCommandFramework - - - - - + + diff --git a/samples/WebhookClient/_WebhookClient.csproj b/samples/WebhookClient/_WebhookClient.csproj index 91131894d..515fcf3a4 100644 --- a/samples/WebhookClient/_WebhookClient.csproj +++ b/samples/WebhookClient/_WebhookClient.csproj @@ -2,12 +2,12 @@ Exe - net5.0 + net6.0 WebHookClient - + diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index fea719016..4fdecd254 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -7,6 +7,8 @@ A Discord.Net extension adding support for bot commands. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs index fb266efa6..5b9bfe72b 100644 --- a/src/Discord.Net.Commands/Results/MatchResult.cs +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord.Commands { @@ -12,7 +12,7 @@ namespace Discord.Commands /// /// Gets on which pipeline stage the command may have matched or failed. /// - public IResult? Pipeline { get; } + public IResult Pipeline { get; } /// public CommandError? Error { get; } @@ -21,7 +21,7 @@ namespace Discord.Commands /// public bool IsSuccess => !Error.HasValue; - private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, string errorReason) { Match = match; Error = error; diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 783565e04..41d83bbc8 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -7,6 +7,8 @@ The core components for the Discord.Net library. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index e9ed63e58..a6861c10c 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -58,6 +58,7 @@ namespace Discord #endregion #region General Actions (20XXX) + UnknownTag = 10087, BotsCannotUse = 20001, OnlyBotsCanUse = 20002, CannotSendExplicitContent = 20009, @@ -98,6 +99,8 @@ namespace Discord #region General Request Errors (40XXX) MaximumNumberOfEditsReached = 30046, + MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047, + MaximumNumberOfTagsInAForumChannelReached = 30048, TokenUnauthorized = 40001, InvalidVerification = 40002, OpeningDMTooFast = 40003, @@ -112,6 +115,7 @@ namespace Discord #region Action Preconditions/Checks (50XXX) InteractionHasAlreadyBeenAcknowledged = 40060, + TagNamesMustBeUnique = 40061, MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, @@ -148,6 +152,7 @@ namespace Discord InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, CannotDeleteRequiredCommunityChannel = 50074, + CannotEditStickersWithinAMessage = 50080, InvalidSticker = 50081, CannotExecuteOnArchivedThread = 50083, InvalidThreadNotificationSettings = 50084, diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index ae0fe674b..af4e5ec6a 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -35,6 +35,17 @@ namespace Discord /// int SlowModeInterval { get; } + /// + /// Gets the default auto-archive duration for client-created threads in this channel. + /// + /// + /// The value of this property does not affect API thread creation, it will not respect this value. + /// + /// + /// The default auto-archive duration for thread creation in this channel. + /// + ThreadArchiveDuration DefaultArchiveDuration { get; } + /// /// Bulk-deletes multiple messages. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 1d36a41b9..d921a2474 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -6,7 +6,7 @@ namespace Discord /// /// Represents a generic voice channel in a guild. /// - public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable + public interface IVoiceChannel : IMessageChannel, INestedChannel, IAudioChannel, IMentionable { /// /// Gets the bit-rate that the clients in this voice channel are requested to use. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 4706b629e..775ff9e65 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1173,7 +1173,6 @@ namespace Discord /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index 4b2fa3bee..7219682b7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -89,7 +89,7 @@ namespace Discord /// Gets this events banner image url. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// The cover images url. string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 5bb00797b..4506b66d9 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -56,7 +56,7 @@ namespace Discord Number = 10, /// - /// A . + /// A . /// Attachment = 11 } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 501a0e905..9b3ac8453 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -17,6 +17,16 @@ namespace Discord /// public Optional IsDefaultPermission { get; set; } + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public Optional IsDMEnabled { get; set; } + + /// + /// Gets or sets the default permissions required by a user to execute this application command. + /// + public Optional DefaultMemberPermissions { get; set; } + internal ApplicationCommandProperties() { } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index c7a7cf741..59040dd4e 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -31,6 +31,16 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; /// @@ -44,7 +54,9 @@ namespace Discord var props = new MessageCommandProperties { Name = Name, - IsDefaultPermission = IsDefaultPermission + IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; return props; @@ -73,5 +85,27 @@ namespace Discord IsDefaultPermission = isDefaultPermission; return this; } + + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public MessageCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public MessageCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index bd1078be3..7c82dce55 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -31,6 +31,16 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; /// @@ -42,7 +52,9 @@ namespace Discord var props = new UserCommandProperties { Name = Name, - IsDefaultPermission = IsDefaultPermission + IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; return props; @@ -71,5 +83,27 @@ namespace Discord IsDefaultPermission = isDefaultPermission; return this; } + + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public UserCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public UserCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 72045a52a..58a002649 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -34,6 +34,19 @@ namespace Discord /// bool IsDefaultPermission { get; } + /// + /// Indicates whether the command is available in DMs with the app. + /// + /// + /// Only for globally-scoped commands. + /// + bool IsEnabledInDm { get; } + + /// + /// Set of default required to invoke the command. + /// + GuildPermissions DefaultMemberPermissions { get; } + /// /// Gets a collection of options for this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 8f6bef995..9017d310f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -55,7 +55,7 @@ namespace Discord string UserLocale { get; } /// - /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. + /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. /// /// /// Non-community guilds (With no locale setting available) will have en-US as the default value sent by Discord. diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 7becca0e0..37342b039 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -195,7 +195,7 @@ namespace Discord /// /// The button to add. /// The row to add the button. - /// There is no more row to add a menu. + /// There is no more row to add a button. /// must be less than . /// The current builder. public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) @@ -348,6 +348,100 @@ namespace Discord return this; } + /// + /// Adds a to the . + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled)); + } + + /// + /// Adds a to the . + /// + /// The menu to add. + /// A Select Menu cannot exist in a pre-occupied ActionRow. + /// The current builder. + public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) + { + if (menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (Components.Count != 0) + throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); + + AddComponent(builtMenu); + + return this; + } + + /// + /// Adds a with specified parameters to the . + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The current builder. + public ActionRowBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled); + + return WithButton(button); + } + + /// + /// Adds a to the . + /// + /// The button to add. + /// Components count reached . + /// A button cannot be added to a row with a SelectMenu. + /// The current builder. + public ActionRowBuilder WithButton(ButtonBuilder button) + { + var builtButton = button.Build(); + + if(Components.Count >= 5) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); + + AddComponent(builtButton); + + return this; + } + /// /// Builds the current builder to a that can be used within a /// @@ -1194,9 +1288,9 @@ namespace Discord /// /// Gets or sets the default value of the text input. /// - /// is less than 0. + /// .Length is less than 0. /// - /// is greater than or . + /// .Length is greater than or . /// public string Value { @@ -1227,7 +1321,7 @@ namespace Discord /// The text input's minimum length. /// The text input's maximum length. /// The text input's required value. - public TextInputBuilder (string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, + public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int? minLength = null, int? maxLength = null, bool? required = null, string value = null) { Label = label; @@ -1291,7 +1385,7 @@ namespace Discord Placeholder = placeholder; return this; } - + /// /// Sets the value of the current builder. /// @@ -1306,18 +1400,18 @@ namespace Discord /// /// Sets the minimum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMinLength(int minLength) { MinLength = minLength; return this; } - + /// /// Sets the maximum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMaxLength(int maxLength) { diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs index 3a3e3cc49..817f69415 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs @@ -64,18 +64,18 @@ namespace Discord /// /// Sets the custom id of the current modal. /// - /// The value to set the custom id to. + /// The value to set the custom id to. /// The current builder. public ModalBuilder WithCustomId(string customId) { CustomId = customId; return this; } - + /// /// Adds a component to the current builder. /// - /// The component to add. + /// The component to add. /// The current builder. public ModalBuilder AddTextInput(TextInputBuilder component) { @@ -213,7 +213,7 @@ namespace Discord /// Adds a to the at the specific row. /// If the row cannot accept the component then it will add it to a row that can. /// - /// The to add. + /// The to add. /// The row to add the text input. /// There are no more rows to add a text input to. /// must be less than . diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index ccfb2da0a..bf74a160c 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -81,6 +81,16 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; private string _description; private List _options; @@ -96,6 +106,8 @@ namespace Discord Name = Name, Description = Description, IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; if (Options != null && Options.Any()) @@ -145,6 +157,28 @@ namespace Discord return this; } + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public SlashCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + /// /// Adds an option to the current slash command. /// @@ -164,21 +198,13 @@ namespace Discord string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { - // Make sure the name matches the requirements from discord - Preconditions.NotNullOrEmpty(name, nameof(name)); - Preconditions.AtLeast(name.Length, 1, nameof(name)); - Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + Preconditions.Options(name, description); // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - // same with description - Preconditions.NotNullOrEmpty(description, nameof(description)); - Preconditions.AtLeast(description.Length, 1, nameof(description)); - Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); - // make sure theres only one option with default set to true if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true) throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); @@ -214,6 +240,7 @@ namespace Discord throw new InvalidOperationException($"Cannot have more than {MaxOptionsCount} options!"); Preconditions.NotNull(option, nameof(option)); + Preconditions.Options(option.Name, option.Description); // this is a double-check when this method is called via AddOption(string name... ) Options.Add(option); return this; @@ -236,6 +263,9 @@ namespace Discord if (Options.Count + options.Length > MaxOptionsCount) throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!"); + foreach (var option in options) + Preconditions.Options(option.Name, option.Description); + Options.AddRange(options); return this; } @@ -379,7 +409,7 @@ namespace Discord MinValue = MinValue, MaxValue = MaxValue }; - } + } /// /// Adds an option to the current slash command. @@ -400,21 +430,13 @@ namespace Discord string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { - // Make sure the name matches the requirements from discord - Preconditions.NotNullOrEmpty(name, nameof(name)); - Preconditions.AtLeast(name.Length, 1, nameof(name)); - Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + Preconditions.Options(name, description); // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - // same with description - Preconditions.NotNullOrEmpty(description, nameof(description)); - Preconditions.AtLeast(description.Length, 1, nameof(description)); - Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); - // make sure theres only one option with default set to true if (isDefault && Options?.Any(x => x.IsDefault == true) == true) throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); @@ -449,6 +471,7 @@ namespace Discord throw new InvalidOperationException($"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); Preconditions.NotNull(option, nameof(option)); + Preconditions.Options(option.Name, option.Description); // double check again Options.Add(option); return this; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 0304120f5..1e2a7b0d7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Discord.Utils; +using Newtonsoft.Json; namespace Discord { @@ -155,6 +156,55 @@ namespace Discord } } + /// + /// Tries to parse a string into an . + /// + /// The json string to parse. + /// The with populated values. An empty instance if method returns . + /// if was succesfully parsed. if not. + public static bool TryParse(string json, out EmbedBuilder builder) + { + builder = new EmbedBuilder(); + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + { + builder = model.ToEmbedBuilder(); + return true; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Parses a string into an . + /// + /// The json string to parse. + /// An with populated values from the passed . + /// Thrown if the string passed is not valid json. + public static EmbedBuilder Parse(string json) + { + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + return model.ToEmbedBuilder(); + + return new EmbedBuilder(); + } + catch + { + throw; + } + } + /// /// Sets the title of an . /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs index 029910e56..7fdc448ad 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -27,6 +27,12 @@ namespace Discord /// public Optional GuildId { get; internal set; } + /// + /// Gets whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message + /// Defaults to true. + /// + public Optional FailIfNotExists { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -39,16 +45,21 @@ namespace Discord /// /// The ID of the guild that will be referenced. It will be validated if sent. /// - public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null) + /// + /// Whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message. Defaults to true. + /// + public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null, bool? failIfNotExists = null) { MessageId = messageId ?? Optional.Create(); InternalChannelId = channelId ?? Optional.Create(); GuildId = guildId ?? Optional.Create(); + FailIfNotExists = failIfNotExists ?? Optional.Create(); } private string DebuggerDisplay => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + - $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}" + + $"{(FailIfNotExists.IsSpecified ? $", FailIfNotExists: ({FailIfNotExists.Value})" : "")}"; public override string ToString() => DebuggerDisplay; diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 935b956c3..5411f5ebf 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -79,7 +79,7 @@ namespace Discord /// Sets a timestamp how long a user should be timed out for. /// /// - /// or a time in the past to clear a currently existing timeout. + /// or a time in the past to clear a currently existing timeout. /// public Optional TimedOutUntil { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 96de06ed8..9703eafe7 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -104,7 +104,7 @@ namespace Discord /// Gets the date and time that indicates if and for how long a user has been timed out. /// /// - /// or a timestamp in the past if the user is not timed out. + /// or a timestamp in the past if the user is not timed out. /// /// /// A indicating how long the user will be timed out for. @@ -116,7 +116,7 @@ namespace Discord /// /// /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); @@ -151,7 +151,7 @@ namespace Discord /// If the user does not have a guild avatar, this will be the user's regular avatar. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// /// A string representing the URL of the displayed avatar for this user. if the user does not have an avatar in place. /// diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index dc2a06540..d9ad43f0d 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -37,8 +37,9 @@ namespace Discord /// Sanitizes the string, safely escaping any Markdown sequences. public static string Sanitize(string text) { - foreach (string unsafeChar in SensitiveCharacters) - text = text.Replace(unsafeChar, $"\\{unsafeChar}"); + if (text != null) + foreach (string unsafeChar in SensitiveCharacters) + text = text.Replace(unsafeChar, $"\\{unsafeChar}"); return text; } diff --git a/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs new file mode 100644 index 000000000..f9a3a3183 --- /dev/null +++ b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a container for temporarily storing CustomId wild card matches of a component. + /// + public interface IRouteMatchContainer + { + /// + /// Gets the collection of captured route segments in this container. + /// + /// + /// A collection of captured route segments. + /// + IEnumerable SegmentMatches { get; } + + /// + /// Sets the property of this container. + /// + /// The collection of captured route segments. + void SetSegmentMatches(IEnumerable segmentMatches); + } +} diff --git a/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs new file mode 100644 index 000000000..675bd6754 --- /dev/null +++ b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Represents an object for storing a CustomId wild card match. + /// + public interface IRouteSegmentMatch + { + /// + /// Gets the captured value of this wild card match. + /// + /// + /// The value of this wild card. + /// + string Value { get; } + } +} diff --git a/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs new file mode 100644 index 000000000..f1d80cfea --- /dev/null +++ b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Represents an object for storing a CustomId wild card match. + /// + internal record RouteSegmentMatch : IRouteSegmentMatch + { + /// + public string Value { get; } + + public RouteSegmentMatch(string value) + { + Value = value; + } + } +} diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index ff8eb7c0d..2f24e660d 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -297,5 +297,22 @@ namespace Discord } } #endregion + + #region SlashCommandOptions + + /// or is null. + /// or are either empty or their length exceed limits. + public static void Options(string name, string description) + { + // Make sure the name matches the requirements from discord + NotNullOrEmpty(name, nameof(name)); + NotNullOrEmpty(description, nameof(description)); + AtLeast(name.Length, 1, nameof(name)); + AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + AtLeast(description.Length, 1, nameof(description)); + AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } + + #endregion } } diff --git a/src/Discord.Net.Core/Utils/UrlValidation.cs b/src/Discord.Net.Core/Utils/UrlValidation.cs index 8e877bd4e..55ae3bdf7 100644 --- a/src/Discord.Net.Core/Utils/UrlValidation.cs +++ b/src/Discord.Net.Core/Utils/UrlValidation.cs @@ -23,7 +23,7 @@ namespace Discord.Utils /// /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord - /// should be used everything other than url buttons. + /// should be used everything other than url buttons. /// /// The URL to validate before sending to discord. /// A URL must include a protocol (either http, https, or discord). diff --git a/src/Discord.Net.Examples/Discord.Net.Examples.csproj b/src/Discord.Net.Examples/Discord.Net.Examples.csproj index b4a336f9f..1bdca7992 100644 --- a/src/Discord.Net.Examples/Discord.Net.Examples.csproj +++ b/src/Discord.Net.Examples/Discord.Net.Examples.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs index e17c9ff14..c8a3428db 100644 --- a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Set the to . + /// Set the to . /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class AutocompleteAttribute : Attribute @@ -14,7 +14,7 @@ namespace Discord.Interactions public Type AutocompleteHandlerType { get; } /// - /// Set the to and define a to handle + /// Set the to and define a to handle /// Autocomplete interactions targeting the parameter this is applied to. /// /// @@ -29,7 +29,7 @@ namespace Discord.Interactions } /// - /// Set the to without specifying a . + /// Set the to without specifying a . /// public AutocompleteAttribute() { } } diff --git a/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs new file mode 100644 index 000000000..ec79da1e3 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the of an application command or module. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class DefaultMemberPermissionsAttribute : Attribute + { + /// + /// Gets the default permission required to use this command. + /// + public GuildPermission Permissions { get; } + + /// + /// Sets the of an application command or module. + /// + /// The default permission required to use this command. + public DefaultMemberPermissionsAttribute(GuildPermission permissions) + { + Permissions = permissions; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs index ed0a532be..2e03dfac6 100644 --- a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs @@ -6,6 +6,7 @@ namespace Discord.Interactions /// Set the "Default Permission" property of an Application Command. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [Obsolete($"Soon to be deprecated, use Permissions-v2 attributes like {nameof(EnabledInDmAttribute)} and {nameof(DefaultMemberPermissionsAttribute)}")] public class DefaultPermissionAttribute : Attribute { /// diff --git a/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs new file mode 100644 index 000000000..a97f85a25 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the property of an application command or module. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class EnabledInDmAttribute : Attribute + { + /// + /// Gets whether or not this command can be used in DMs. + /// + public bool IsEnabled { get; } + + /// + /// Sets the property of an application command or module. + /// + /// Whether or not this command can be used in DMs. + public EnabledInDmAttribute(bool isEnabled) + { + IsEnabled = isEnabled; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index d611b574d..e9b877268 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -21,9 +21,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// The label of the input. /// The custom id of the input. - /// Whether the user is required to input a value.> protected ModalInputAttribute(string customId) { CustomId = customId; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 35121cd6b..4439e1d84 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -36,7 +36,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// + /// The custom id of the text input.> /// The style of the text input. /// The placeholder of the text input. /// The minimum length of the text input's content. diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 77d6e8f25..0f6ecfc66 100644 --- a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -29,7 +29,7 @@ namespace Discord.Interactions /// /// This precondition will always fail if the command is being invoked in a . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// @@ -41,7 +41,7 @@ namespace Discord.Interactions /// /// Requires that the user invoking the command to have a specific . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// diff --git a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs index d40547b3c..be0e5eb70 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs @@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders /// /// Gets the default permission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + internal ContextCommandBuilder (ModuleBuilder module) : base(module) { } /// @@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public ContextCommandBuilder SetDefaultPermission (bool defaultPermision) { DefaultPermission = defaultPermision; @@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ContextCommandBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ContextCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + internal override ContextCommandInfo Build (ModuleInfo module, InteractionService commandService) => ContextCommandInfo.Create(this, module, commandService); } diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs index d8e9b0658..c21fd5ae8 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs @@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders /// /// Gets and sets the default permission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + internal SlashCommandBuilder (ModuleBuilder module) : base(module) { } /// @@ -45,10 +56,11 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public SlashCommandBuilder WithDefaultPermission (bool permission) { DefaultPermission = permission; @@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + internal override SlashCommandInfo Build (ModuleInfo module, InteractionService commandService) => new SlashCommandInfo(this, module, commandService); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 340119ddd..8dd2c4004 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index fc1dbdc0e..c13ff40de 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders } /// - /// Adds text components to . + /// Adds text components to . /// /// Text Component builder factory. /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index 40c263643..0eb91ee6a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -51,8 +51,19 @@ namespace Discord.Interactions.Builders /// /// Gets and sets the default permission of this module. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + /// /// Gets and sets whether this has a . /// @@ -159,12 +170,39 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public ModuleBuilder WithDefaultPermission (bool permission) { DefaultPermission = permission; return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModuleBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModuleBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + /// /// Adds attributes to . /// @@ -319,7 +357,8 @@ namespace Discord.Interactions.Builders return this; } - + + /// /// Adds a modal command builder to . /// /// factory. diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index b2317d1f3..1bbdfcc4a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -85,6 +85,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.AddPreconditions(precondition); break; @@ -169,6 +179,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defaultPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; @@ -211,6 +231,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defaultPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs index 78d007d44..fec1a6ce9 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs @@ -122,7 +122,7 @@ namespace Discord.Interactions.Builders /// /// Adds preconditions to /// - /// New attributes to be added to . + /// New attributes to be added to . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj index c617eff61..a3ac3d508 100644 --- a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj +++ b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj @@ -7,8 +7,10 @@ Discord.Interactions Discord.Net.Interactions A Discord.Net extension adding support for Application Commands. + 5 + True - + diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs index 4c2e7af7d..2d6d748d4 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs @@ -17,6 +17,12 @@ namespace Discord.Interactions /// public bool DefaultPermission { get; } + /// + public bool IsEnabledInDm { get; } + + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// public override IReadOnlyCollection Parameters { get; } @@ -31,6 +37,8 @@ namespace Discord.Interactions { CommandType = builder.CommandType; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); } diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index a123ac183..e428144c7 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -26,6 +26,12 @@ namespace Discord.Interactions /// public bool DefaultPermission { get; } + /// + public bool IsEnabledInDm { get; } + + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// public override IReadOnlyCollection Parameters { get; } @@ -41,6 +47,8 @@ namespace Discord.Interactions { Description = builder.Description; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs index 1e0d532b0..dd1b97899 100644 --- a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace Discord.Interactions { /// @@ -18,6 +20,17 @@ namespace Discord.Interactions /// /// Gets the DefaultPermission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] bool DefaultPermission { get; } + + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; } + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; } } } diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 321e0bfa9..4f40f1607 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -41,8 +41,19 @@ namespace Discord.Interactions /// /// Gets the default Permission of this module. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; } + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; } + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// /// Gets the collection of Sub Modules of this module. /// @@ -110,6 +121,8 @@ namespace Discord.Interactions Description = builder.Description; Parent = parent; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = BuildDefaultMemberPermissions(builder); SlashCommands = BuildSlashCommands(builder).ToImmutableArray(); ContextCommands = BuildContextCommands(builder).ToImmutableArray(); ComponentCommands = BuildComponentCommands(builder).ToImmutableArray(); @@ -226,5 +239,20 @@ namespace Discord.Interactions } return true; } + + private static GuildPermission? BuildDefaultMemberPermissions(ModuleBuilder builder) + { + var permissions = builder.DefaultMemberPermissions; + + var parent = builder.Parent; + + while (parent != null) + { + permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0).SanitizeGuildPermissions(); + parent = parent.Parent; + } + + return permissions; + } } } diff --git a/src/Discord.Net.Interactions/InteractionContext.cs b/src/Discord.Net.Interactions/InteractionContext.cs index 99a8d8736..b81cc5938 100644 --- a/src/Discord.Net.Interactions/InteractionContext.cs +++ b/src/Discord.Net.Interactions/InteractionContext.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + namespace Discord.Interactions { /// - public class InteractionContext : IInteractionContext + public class InteractionContext : IInteractionContext, IRouteMatchContainer { /// public IDiscordClient Client { get; } @@ -13,14 +16,15 @@ namespace Discord.Interactions public IUser User { get; } /// public IDiscordInteraction Interaction { get; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } /// /// Initializes a new . /// /// The underlying client. /// The underlying interaction. - /// who executed the command. - /// the command originated from. + /// the command originated from. public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IMessageChannel channel = null) { Client = client; @@ -30,5 +34,12 @@ namespace Discord.Interactions User = interaction.User; Interaction = interaction; } + + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; } } diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs index 873f4c173..a14779dbb 100644 --- a/src/Discord.Net.Interactions/InteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs @@ -45,7 +45,7 @@ namespace Discord.Interactions protected virtual async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => await Context.Interaction.DeferAsync(ephemeral, options).ConfigureAwait(false); - /// + /// protected virtual async Task RespondAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -70,7 +70,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task FollowupAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -95,7 +95,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task ReplyAsync (string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null) => await Context.Channel.SendMessageAsync(text, false, embed, options, allowedMentions, messageReference, components).ConfigureAwait(false); @@ -118,9 +118,9 @@ namespace Discord.Interactions /// protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal); - /// - protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where T : class, IModal - => await Context.Interaction.RespondWithModalAsync(customId, options); + /// + protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where TModal : class, IModal + => await Context.Interaction.RespondWithModalAsync(customId, options); //IInteractionModuleBase diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 01fb8cc9d..f57c75a31 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -223,7 +223,8 @@ namespace Discord.Interactions new ConcurrentDictionary { [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) }); _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), @@ -234,7 +235,8 @@ namespace Discord.Interactions [typeof(IUser)] = typeof(DefaultUserReader<>), [typeof(IMessage)] = typeof(DefaultMessageReader<>), [typeof(IConvertible)] = typeof(DefaultValueReader<>), - [typeof(Enum)] = typeof(EnumReader<>) + [typeof(Enum)] = typeof(EnumReader<>), + [typeof(Nullable<>)] = typeof(NullableReader<>) }); } @@ -421,7 +423,7 @@ namespace Discord.Interactions /// /// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// /// The target guild. /// Commands to be registered to Discord. @@ -517,7 +519,7 @@ namespace Discord.Interactions /// /// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// /// Commands to be registered to Discord. /// @@ -775,6 +777,9 @@ namespace Discord.Interactions await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } + + SetMatchesIfApplicable(context, result); + return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } @@ -819,9 +824,30 @@ namespace Discord.Interactions await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } + + SetMatchesIfApplicable(context, result); + return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } + private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) + where T : class, ICommandInfo + { + if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) + return; + + if (searchResult.RegexCaptureGroups?.Length > 0) + { + var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; + for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) + matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + + matchContainer.SetSegmentMatches(matches); + } + else + matchContainer.SetSegmentMatches(Array.Empty()); + } + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) => _typeConverterMap.Get(type, services); @@ -941,7 +967,7 @@ namespace Discord.Interactions /// Removes a type reader for the given type. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -954,7 +980,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the type . /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the readers from. @@ -967,7 +993,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the given type. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -980,7 +1006,7 @@ namespace Discord.Interactions /// Serialize an object using a into a to be placed in a Component CustomId. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// Type of the object to be serialized. diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index e83c91fef..b570e6d84 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -87,12 +87,12 @@ namespace Discord.Interactions await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } - protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) + protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) { if (Context.Interaction is not RestInteraction restInteraction) throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); - var payload = restInteraction.RespondWithModal(customId, options); + var payload = restInteraction.RespondWithModal(customId, options); if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); diff --git a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs index bd89bf6b7..a9a12ee33 100644 --- a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs +++ b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Represents a result type for . + /// Represents a result type for . /// public struct TypeConverterResult : IResult { diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs new file mode 100644 index 000000000..ba6568ad1 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableComponentConverter : ComponentTypeConverter + { + private readonly ComponentTypeConverter _typeConverter; + + public NullableComponentConverter(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeConverter = interactionService.GetComponentTypeConverter(type, services); + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs new file mode 100644 index 000000000..ed88dc64a --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableReader : TypeReader + { + private readonly TypeReader _typeReader; + + public NullableReader(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeReader = interactionService.GetTypeReader(type, services); + } + + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + => string.IsNullOrEmpty(option) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeReader.ReadAsync(context, option, services); + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index c2052b7c7..e4b6f893c 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -40,7 +40,8 @@ namespace Discord.Interactions { Name = commandInfo.Name, Description = commandInfo.Description, - IsDefaultPermission = commandInfo.DefaultPermission, + IsDMEnabled = commandInfo.IsEnabledInDm, + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), }.Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) @@ -64,8 +65,20 @@ namespace Discord.Interactions public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) => commandInfo.CommandType switch { - ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(), - ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(), + ApplicationCommandType.Message => new MessageCommandBuilder + { + Name = commandInfo.Name, + IsDefaultPermission = commandInfo.DefaultPermission, + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), + IsDMEnabled = commandInfo.IsEnabledInDm + }.Build(), + ApplicationCommandType.User => new UserCommandBuilder + { + Name = commandInfo.Name, + IsDefaultPermission = commandInfo.DefaultPermission, + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), + IsDMEnabled = commandInfo.IsEnabledInDm + }.Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") }; #endregion @@ -113,6 +126,8 @@ namespace Discord.Interactions Name = moduleInfo.SlashGroupName, Description = moduleInfo.Description, IsDefaultPermission = moduleInfo.DefaultPermission, + IsDMEnabled = moduleInfo.IsEnabledInDm, + DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions }.Build(); if (options.Count > SlashCommandBuilder.MaxOptionsCount) @@ -217,5 +232,8 @@ namespace Discord.Interactions return builder.Build(); } + + public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) => + permissions == 0 ? null : permissions; } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 81598b96e..8b84149dd 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -24,5 +24,12 @@ namespace Discord.API [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } + + // V2 Permissions + [JsonProperty("dm_permission")] + public Optional DmPermission { get; set; } + + [JsonProperty("default_member_permissions")] + public Optional DefaultMemberPermission { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index f5d57a942..d9d7d469c 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -70,7 +70,8 @@ namespace Discord.API //ForumChannel [JsonProperty("available_tags")] public Optional ForumTags { get; set; } + [JsonProperty("default_auto_archive_duration")] - public Optional DefaultAutoArchiveDuration { get; set; } + public Optional AutoArchiveDuration { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs index 6cc7603e0..70ef4e678 100644 --- a/src/Discord.Net.Rest/API/Common/MessageReference.cs +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -12,5 +12,8 @@ namespace Discord.API [JsonProperty("guild_id")] public Optional GuildId { get; set; } + + [JsonProperty("fail_if_not_exists")] + public Optional FailIfNotExists { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 82f0befcd..7ae8718b6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -19,6 +19,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("dm_permission")] + public Optional DmPermission { get; set; } + + [JsonProperty("default_member_permissions")] + public Optional DefaultMemberPermission { get; set; } + public CreateApplicationCommandParams() { } public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) { diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 837fd1d04..59e1f0b4b 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Integration")] [assembly: InternalsVisibleTo("Discord.Net.Interactions")] [assembly: TypeForwardedTo(typeof(Discord.Embed))] diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 98692998f..bec2396ef 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -7,6 +7,8 @@ A core Discord.Net library containing the REST client and models. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a6fdfc399..e179675ba 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -173,10 +173,12 @@ namespace Discord.API private async Task LogoutInternalAsync() { //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. - if (LoginState == LoginState.LoggedOut) return; + if (LoginState == LoginState.LoggedOut) + return; LoginState = LoginState.LoggingOut; - try { _loginCancelToken?.Cancel(false); } + try + { _loginCancelToken?.Cancel(false); } catch { } await DisconnectInternalAsync(null).ConfigureAwait(false); @@ -398,7 +400,7 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); @@ -414,9 +416,9 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); - if(args.Topic.IsSpecified) + if (args.Topic.IsSpecified) Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name)); Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval)); @@ -689,9 +691,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); + var bucket = new BucketIds(channelId: channelId); + try { - await SendAsync("DELETE", $"stage-instances/{channelId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"stage-instances/{channelId}", bucket, options: options).ConfigureAwait(false); } catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) { } } @@ -816,9 +820,11 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -834,12 +840,12 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null) + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -855,11 +861,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// This operation may only be called with a token. - public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null) + public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -870,7 +876,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", ids, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . @@ -891,7 +897,7 @@ namespace Discord.API /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -911,7 +917,7 @@ namespace Discord.API } var ids = new BucketIds(webhookId: webhookId); - return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -1398,7 +1404,7 @@ namespace Discord.API if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.File.IsSpecified) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if(args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -1418,7 +1424,7 @@ namespace Discord.API throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - + var ids = new BucketIds(); return await SendMultipartAsync("POST", () => $"webhooks/{CurrentApplicationId}/{token}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } @@ -1747,8 +1753,10 @@ namespace Discord.API if (args.TargetType.IsSpecified) { Preconditions.NotEqual((int)args.TargetType.Value, (int)TargetUserType.Undefined, nameof(args.TargetType)); - if (args.TargetType.Value == TargetUserType.Stream) Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); - if (args.TargetType.Value == TargetUserType.EmbeddedApplication) Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); + if (args.TargetType.Value == TargetUserType.Stream) + Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); + if (args.TargetType.Value == TargetUserType.EmbeddedApplication) + Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); } options = RequestOptions.CreateOrClone(options); @@ -2432,6 +2440,18 @@ namespace Discord.API return (expr as MemberExpression).Member.Name; } + + private static string WebhookQuery(bool wait = false, ulong? threadId = null) + { + List querys = new List() { }; + if (wait) + querys.Add("wait=true"); + if (threadId.HasValue) + querys.Add($"thread_id={threadId}"); + + return $"{string.Join("&", querys)}"; + } + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b1948f80a..7cb15bed1 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -32,9 +32,15 @@ namespace Discord.Rest /// Initializes a new with the provided configuration. /// /// The configuration to be used with the client. - public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } // used for socket client rest access - internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, serializer: Serializer, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); @@ -82,6 +88,8 @@ namespace Discord.Rest #region Rest interactions + private readonly bool _apiOnCreation; + public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, string body) => IsValidHttpInteraction(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, byte[] body) @@ -113,8 +121,8 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body) - => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); + public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func doApiCallOnCreation = null) + => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation); /// /// Creates a from a http message. @@ -127,7 +135,7 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body) + public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func doApiCallOnCreation = null) { if (!IsValidHttpInteraction(publicKey, signature, timestamp, body)) { @@ -138,12 +146,12 @@ namespace Discord.Rest using (var jsonReader = new JsonTextReader(textReader)) { var model = Serializer.Deserialize(jsonReader); - return await RestInteraction.CreateAsync(this, model); + return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation != null ? doApiCallOnCreation(model.Type) : _apiOnCreation); } } #endregion - + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 7bf7440ce..a09d9ee98 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -9,5 +9,7 @@ namespace Discord.Rest { /// Gets or sets the provider used to generate new REST connections. public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; + + public bool APIOnRestInteractionCreation { get; set; } = true; } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index fc807cac0..7246ac197 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BanAuditLogData(RestUser.Create(discord, userInfo)); + return new BanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was banned. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the banned user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs index 0d12e4609..288cb9d0a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + return new BotAddAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the bot that was added. /// + /// + /// Will be if the bot is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the bot. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index b177b2435..3560b9a27 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 9d0aed12b..2dc2f22f6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs index dceb73d0a..b533f0268 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,12 +18,15 @@ namespace Discord.Rest internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new KickAuditLogData(RestUser.Create(discord, userInfo)); + return new KickAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was kicked. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the kicked user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs index 763c90c68..276604d03 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -27,7 +27,7 @@ namespace Discord.Rest .ToList(); var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, userInfo); + RestUser user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs index f22b83e4c..f3437e621 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -33,7 +33,7 @@ namespace Discord.Rest newMute = muteModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, targetInfo); + RestUser user = (targetInfo != null) ? RestUser.Create(discord, targetInfo) : null; var before = new MemberInfo(oldNick, oldDeaf, oldMute); var after = new MemberInfo(newNick, newDeaf, newMute); @@ -44,6 +44,9 @@ namespace Discord.Rest /// /// Gets the user that the changes were performed on. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user who the changes were performed on. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index 66b3f7d83..746fc2ea6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -2,6 +2,7 @@ using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; +using System; namespace Discord.Rest { @@ -20,7 +21,7 @@ namespace Discord.Rest internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, userInfo != null ? RestUser.Create(discord, userInfo) : null); } /// @@ -41,6 +42,9 @@ namespace Discord.Rest /// /// Gets the user of the messages that were deleted. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the deleted messages. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs index be66ac846..c33fd5f44 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was pinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the pinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs index b4fa389cc..f6fd31771 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was unpinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the unpinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs index bc7e7fd4f..f12d9a1af 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,7 +18,7 @@ namespace Discord.Rest internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); + return new UnbanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs index c01df96fd..b34afd027 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs @@ -12,7 +12,11 @@ namespace Discord.Rest public class RestStageChannel : RestVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -37,13 +41,11 @@ namespace Discord.Rest IsLive = isLive; if(isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 76c75ab6e..81f21bcd7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -21,11 +21,12 @@ namespace Discord.Rest public virtual int SlowModeInterval { get; private set; } /// public ulong? CategoryId { get; private set; } - /// public string Mention => MentionUtils.MentionChannel(Id); /// public bool IsNsfw { get; private set; } + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) @@ -46,6 +47,12 @@ namespace Discord.Rest if (model.SlowMode.IsSpecified) SlowModeInterval = model.SlowMode.Value; IsNsfw = model.Nsfw.GetValueOrDefault(); + + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// @@ -86,25 +93,25 @@ namespace Discord.Rest => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); /// - public Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual 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) + public virtual 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) + public virtual 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) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -136,7 +143,7 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -146,7 +153,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -156,7 +163,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -166,35 +173,35 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); /// @@ -231,38 +238,6 @@ namespace Discord.Rest public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public virtual Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion - - #region Invites - /// - public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - /// - public virtual async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - /// /// Creates a thread within this . /// @@ -299,6 +274,38 @@ namespace Discord.Rest var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); return RestThreadChannel.Create(Discord, Guild, model); } + + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public virtual Task GetCategoryAsync(RequestOptions options = null) + => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion + + #region Invites + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; #endregion #region ITextChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index bcf03a5bc..31d313a48 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -2,6 +2,7 @@ using Discord.Audio; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -12,21 +13,21 @@ namespace Discord.Rest /// Represents a REST-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel + public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel { #region RestVoiceChannel + /// + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. + /// + public virtual bool IsTextInVoice + => Guild.Features.HasTextInVoice; /// public int Bitrate { get; private set; } /// public int? UserLimit { get; private set; } - /// - public ulong? CategoryId { get; private set; } /// public string RTCRegion { get; private set; } - /// - public string Mention => MentionUtils.MentionChannel(Id); - internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -41,7 +42,6 @@ namespace Discord.Rest internal override void Update(Model model) { base.Update(model); - CategoryId = model.CategoryId; if(model.Bitrate.IsSpecified) Bitrate = model.Bitrate.Value; @@ -59,41 +59,185 @@ namespace Discord.Rest Update(model); } - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion + /// + /// Cannot modify text channel properties of a voice channel. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties of a voice channel"); - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// + /// Cannot create a thread within a voice channel. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Cannot create a thread within a voice channel"); + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + #endregion + #region IAudioChannel /// /// Connecting to a REST-based channel is not supported. diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 469e93db4..8bab35937 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -132,12 +132,15 @@ namespace Discord.Rest } public static ulong GetUploadLimit(IGuild guild) { - return guild.PremiumTier switch + var tierFactor = guild.PremiumTier switch { - PremiumTier.Tier2 => 50ul * 1000000, - PremiumTier.Tier3 => 100ul * 1000000, - _ => 8ul * 1000000 + PremiumTier.Tier2 => 50, + PremiumTier.Tier3 => 100, + _ => 8 }; + + var mebibyte = Math.Pow(2, 20); + return (ulong) (tierFactor * mebibyte); } #endregion @@ -151,7 +154,7 @@ namespace Discord.Rest if (fromUserId.HasValue) return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); - else + else return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); } @@ -908,7 +911,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); - + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 92d598466..974ea69ad 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1161,7 +1161,6 @@ namespace Discord.Rest /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 196416f0e..22e56a733 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -39,16 +39,16 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestCommandBase(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs index 4227c802a..828299d22 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs @@ -27,20 +27,20 @@ namespace Discord.Rest { } - internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestCommandBaseData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { Name = model.Name; if (model.Resolved.IsSpecified && ResolvableData == null) { ResolvableData = new RestResolvableData(); - await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false); + await ResolvableData.PopulateAsync(client, guild, channel, model, doApiCall).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs index 9353a8530..72b894729 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs @@ -22,7 +22,7 @@ namespace Discord.Rest internal readonly Dictionary Attachments = new Dictionary(); - internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model) + internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model, bool doApiCall) { var resolved = model.Resolved.Value; @@ -38,15 +38,26 @@ namespace Discord.Rest if (resolved.Channels.IsSpecified) { - var channels = await guild.GetChannelsAsync().ConfigureAwait(false); + var channels = doApiCall ? await guild.GetChannelsAsync().ConfigureAwait(false) : null; foreach (var channelModel in resolved.Channels.Value) { - var restChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); + if (channels != null) + { + var guildChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); - restChannel.Update(channelModel.Value); + guildChannel.Update(channelModel.Value); - Channels.Add(ulong.Parse(channelModel.Key), restChannel); + Channels.Add(ulong.Parse(channelModel.Key), guildChannel); + } + else + { + var restChannel = RestChannel.Create(discord, channelModel.Value); + + restChannel.Update(channelModel.Value); + + Channels.Add(ulong.Parse(channelModel.Key), restChannel); + } } } @@ -76,7 +87,10 @@ namespace Discord.Rest { foreach (var msg in resolved.Messages.Value) { - channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value ?? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false)); + channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value + ?? (doApiCall + ? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false) + : null)); RestUser author; diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs index 609fe0829..34c664b09 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs @@ -20,22 +20,22 @@ namespace Discord.Rest } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IMessageCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs index 127d539d9..d2968a38a 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs @@ -23,15 +23,15 @@ namespace Discord.Rest /// Note Not implemented for /// public override IReadOnlyCollection Options - => throw new System.NotImplementedException(); + => throw new NotImplementedException(); internal RestMessageCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestMessageCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs index 7f55fd61b..91319a649 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestUserCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IUserCommandInteractionData diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs index e18499d42..61b291f7c 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs @@ -26,10 +26,10 @@ namespace Discord.Rest internal RestUserCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestUserCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index fb44b101a..522c098e6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -100,7 +100,12 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() + }; if (arg is SlashCommandProperties slashProps) @@ -134,7 +139,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) @@ -171,7 +180,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) @@ -285,7 +298,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 002510eac..e0eab6051 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -37,15 +37,15 @@ namespace Discord.Rest Data = new RestMessageComponentData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageComponent(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient discord, Model model) + internal override async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { - await base.UpdateAsync(discord, model).ConfigureAwait(false); + await base.UpdateAsync(discord, model, doApiCall).ConfigureAwait(false); if (model.Message.IsSpecified && model.ChannelId.IsSpecified) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index 5f54fe051..9229b63b5 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -26,10 +26,10 @@ namespace Discord.Rest Data = new RestModalData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model) + internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) { var entity = new RestModal(client, model); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index ea8d5bc42..667609ef4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -27,6 +27,12 @@ namespace Discord.Rest /// public bool IsDefaultPermission { get; private set; } + /// + public bool IsEnabledInDm { get; private set; } + + /// + public GuildPermissions DefaultMemberPermissions { get; private set; } + /// /// Gets a collection of options for this command. /// @@ -57,6 +63,9 @@ namespace Discord.Rest Options = model.Options.IsSpecified ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 8a8921abe..59adc0347 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -16,6 +16,10 @@ namespace Discord.Rest /// public abstract class RestInteraction : RestEntity, IDiscordInteraction { + // Added so channel & guild methods don't need a client reference + private Func> _getChannel = null; + private Func> _getGuild = null; + /// public InteractionType Type { get; private set; } @@ -31,6 +35,10 @@ namespace Discord.Rest /// /// Gets the user who invoked the interaction. /// + /// + /// If this user is an and is set to false, + /// will return + /// public RestUser User { get; private set; } /// @@ -48,14 +56,38 @@ namespace Discord.Rest public bool IsValidToken => InteractionHelper.CanRespondOrFollowup(this); + /// + /// Gets the ID of the channel this interaction was executed in. + /// + /// + /// if the interaction was not executed in a guild. + /// + public ulong? ChannelId { get; private set; } = null; + /// /// Gets the channel that this interaction was executed in. /// + /// + /// if is set to false. + /// Call to set this property and get the interaction channel. + /// public IRestMessageChannel Channel { get; private set; } /// - /// Gets the guild this interaction was executed in. + /// Gets the ID of the guild this interaction was executed in if applicable. /// + /// + /// if the interaction was not executed in a guild. + /// + public ulong? GuildId { get; private set; } = null; + + /// + /// Gets the guild this interaction was executed in if applicable. + /// + /// + /// This property will be if is set to false + /// or if the interaction was not executed in a guild. + /// public RestGuild Guild { get; private set; } /// @@ -72,11 +104,11 @@ namespace Discord.Rest : DateTime.UtcNow; } - internal static async Task CreateAsync(DiscordRestClient client, Model model) + internal static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { if(model.Type == InteractionType.Ping) { - return await RestPingInteraction.CreateAsync(client, model); + return await RestPingInteraction.CreateAsync(client, model, doApiCall); } if (model.Type == InteractionType.ApplicationCommand) @@ -90,26 +122,26 @@ namespace Discord.Rest return dataModel.Type switch { - ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model).ConfigureAwait(false), + ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), _ => null }; } if (model.Type == InteractionType.MessageComponent) - return await RestMessageComponent.CreateAsync(client, model).ConfigureAwait(false); + return await RestMessageComponent.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ApplicationCommandAutocomplete) - return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false); + return await RestAutocompleteInteraction.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ModalSubmit) - return await RestModal.CreateAsync(client, model).ConfigureAwait(false); + return await RestModal.CreateAsync(client, model, doApiCall).ConfigureAwait(false); return null; } - internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model) + internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { IsDMInteraction = !model.GuildId.IsSpecified; @@ -120,16 +152,23 @@ namespace Discord.Rest Version = model.Version; Type = model.Type; - if(Guild == null && model.GuildId.IsSpecified) + if (Guild == null && model.GuildId.IsSpecified) { - Guild = await discord.GetGuildAsync(model.GuildId.Value); + GuildId = model.GuildId.Value; + if (doApiCall) + Guild = await discord.GetGuildAsync(model.GuildId.Value); + else + { + Guild = null; + _getGuild = new(async (opt, ul) => await discord.GetGuildAsync(ul, opt)); + } } if (User == null) { if (model.Member.IsSpecified && model.GuildId.IsSpecified) { - User = RestGuildUser.Create(Discord, Guild, model.Member.Value); + User = RestGuildUser.Create(Discord, Guild, model.Member.Value, (Guild is null) ? model.GuildId.Value : null); } else { @@ -137,18 +176,33 @@ namespace Discord.Rest } } - if(Channel == null && model.ChannelId.IsSpecified) + if (Channel == null && model.ChannelId.IsSpecified) { try { - Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + ChannelId = model.ChannelId.Value; + if (doApiCall) + Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + else + { + _getChannel = new(async (opt, ul) => + { + if (Guild is null) + return (IRestMessageChannel)await discord.GetChannelAsync(ul, opt); + else // get a guild channel if the guild is set. + return (IRestMessageChannel)await Guild.GetChannelAsync(ul, opt); + }); + + Channel = null; + } } - catch(HttpException x) when(x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore + catch (HttpException x) when (x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore } UserLocale = model.UserLocale.IsSpecified - ? model.UserLocale.Value - : null; + ? model.UserLocale.Value + : null; + GuildLocale = model.GuildLocale.IsSpecified ? model.GuildLocale.Value : null; @@ -164,6 +218,59 @@ namespace Discord.Rest return json.ToString(); } + /// + /// Gets the channel this interaction was executed in. Will be a DM channel if the interaction was executed in DM. + /// + /// + /// Calling this method succesfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// A Rest channel to send messages to. + /// Thrown if no channel can be received. + public async Task GetChannelAsync(RequestOptions options = null) + { + if (IsDMInteraction && Channel is null) + { + var channel = await User.CreateDMChannelAsync(options); + Channel = channel; + } + + else if (Channel is null) + { + var channel = await _getChannel(options, ChannelId.Value); + + if (channel is null) + throw new InvalidOperationException("The interaction channel was not able to be retrieved."); + Channel = channel; + + _getChannel = null; // get rid of it, we don't need it anymore. + } + + return Channel; + } + + /// + /// Gets the guild this interaction was executed in if applicable. + /// + /// + /// Calling this method succesfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// The guild this interaction was executed in. if the interaction was executed inside DM. + public async Task GetGuildAsync(RequestOptions options) + { + if (IsDMInteraction) + return null; + + if (Guild is null) + Guild = await _getGuild(options, GuildId.Value); + + _getGuild = null; // get rid of it, we don't need it anymore. + return Guild; + } + /// public abstract string Defer(bool ephemeral = false, RequestOptions options = null); /// @@ -333,7 +440,6 @@ namespace Discord.Rest => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); - /// #if NETCOREAPP3_0_OR_GREATER != true /// Task IDiscordInteraction.RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs index bd15bc2d3..47e1a3b0f 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -18,10 +18,10 @@ namespace Discord.Rest { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestPingInteraction(client, model.Id); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs index 24dbae37a..27c536240 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -32,10 +32,10 @@ namespace Discord.Rest Data = new RestAutocompleteInteractionData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestAutocompleteInteraction(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs index 21184fcf6..f955e7855 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestSlashCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //ISlashCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs index f967cc628..19a819ab4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs @@ -14,15 +14,15 @@ namespace Discord.Rest internal RestSlashCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestSlashCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { - await base.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await base.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); Options = model.Options.IsSpecified ? model.Options.Value.Select(x => new RestSlashCommandDataOption(this, x)).ToImmutableArray() diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index c48a60aac..69e038fd2 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -144,7 +144,8 @@ namespace Discord.Rest { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index a2ad4fd77..df629bec7 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -25,7 +25,7 @@ namespace Discord.Rest public string Name { get; private set; } /// public string Icon { get; private set; } - /// /> + /// public Emoji Emoji { get; private set; } /// public GuildPermissions Permissions { get; private set; } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 0a4a33099..6c311b6b5 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -35,7 +35,7 @@ namespace Discord.Rest /// public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); /// - public ulong GuildId => Guild.Id; + public ulong GuildId { get; } /// public bool? IsPending { get; private set; } /// @@ -80,14 +80,16 @@ namespace Discord.Rest /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) + internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong? guildId = null) : base(discord, id) { - Guild = guild; + if (guild is not null) + Guild = guild; + GuildId = guildId ?? Guild.Id; } - internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model) + internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? guildId = null) { - var entity = new RestGuildUser(discord, guild, model.User.Id); + var entity = new RestGuildUser(discord, guild, model.User.Id, guildId); entity.Update(model); return entity; } @@ -116,7 +118,7 @@ namespace Discord.Rest private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(Guild.Id); + roles.Add(GuildId); for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4062cda3d..f5a88486b 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -87,6 +87,7 @@ namespace Discord.Rest ChannelId = entity.InternalChannelId, GuildId = entity.GuildId, MessageId = entity.MessageId, + FailIfNotExists = entity.FailIfNotExists }; } public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) diff --git a/src/Discord.Net.Rest/Extensions/StringExtensions.cs b/src/Discord.Net.Rest/Extensions/StringExtensions.cs new file mode 100644 index 000000000..4981a4298 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/StringExtensions.cs @@ -0,0 +1,47 @@ +using Discord.Net.Converters; +using Newtonsoft.Json; +using System.Linq; +using System; + +namespace Discord.Rest +{ + /// + /// Responsible for formatting certain entities as Json , to reuse later on. + /// + public static class StringExtensions + { + private static Lazy _settings = new(() => + { + var serializer = new JsonSerializerSettings() + { + ContractResolver = new DiscordContractResolver() + }; + serializer.Converters.Add(new EmbedTypeConverter()); + return serializer; + }); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The builder to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this EmbedBuilder builder, Formatting formatting = Formatting.Indented) + => ToJsonString(builder.Build(), formatting); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The embed to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this Embed embed, Formatting formatting = Formatting.Indented) + => JsonConvert.SerializeObject(embed.ToModel(), formatting, _settings.Value); + } +} diff --git a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs index 196c6133b..d407f5103 100644 --- a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs +++ b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; namespace Discord.Rest @@ -6,7 +8,7 @@ namespace Discord.Rest /// /// Represents a Rest based context of an . /// - public class RestInteractionContext : IRestInteractionContext + public class RestInteractionContext : IRestInteractionContext, IRouteMatchContainer where TInteraction : RestInteraction { /// @@ -45,6 +47,9 @@ namespace Discord.Rest /// public Func InteractionResponseCallback { get; set; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } + /// /// Initializes a new . /// @@ -71,6 +76,13 @@ namespace Discord.Rest InteractionResponseCallback = interactionResponseCallback; } + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; + // IInterationContext /// IDiscordClient IInteractionContext.Client => Client; diff --git a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs index cfd64104d..43cd3f902 100644 --- a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs +++ b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs @@ -243,7 +243,7 @@ namespace Discord.Net.ED25519 /// /// // Decode a base58-encoded string into byte array /// - /// Base58 data string + /// Base58 data string /// Byte array public static byte[] Base58Decode(string input) { diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 75e79eec2..4915a5c39 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -60,14 +60,9 @@ namespace Discord.Net.Queue _clearToken?.Cancel(); _clearToken?.Dispose(); _clearToken = new CancellationTokenSource(); - if (_parentToken != null) - { - _requestCancelTokenSource?.Dispose(); - _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); - _requestCancelToken = _requestCancelTokenSource.Token; - } - else - _requestCancelToken = _clearToken.Token; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; } finally { _tokenLock.Release(); } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 2ce89be5b..a4355bc02 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -8,6 +8,8 @@ net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 true + 5 + True diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 21594fed7..cca2de203 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -274,7 +274,7 @@ namespace Discord.API { ["$device"] = "Discord.Net", ["$os"] = Environment.OSVersion.Platform.ToString(), - [$"browser"] = "Discord.Net" + ["$browser"] = "Discord.Net" }; var msg = new IdentifyParams() { diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index aaef4656a..57d58a8b1 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2331,7 +2331,9 @@ namespace Discord.WebSocket SocketUser user = data.User.IsSpecified ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. + : guild != null + ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. + : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); SocketChannel channel = null; if(data.ChannelId.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index c538bc7fe..16ed7b32d 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -223,6 +223,8 @@ namespace Discord.WebSocket #region IChannel /// + string IChannel.Name => Name; + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 91bca5054..56cd92185 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -15,7 +15,11 @@ namespace Discord.WebSocket public class SocketStageChannel : SocketVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -49,19 +53,16 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } - internal void Update(StageInstance model, bool isLive = false) { IsLive = isLive; if (isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e4a299edc..6aece7d78 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -40,7 +40,8 @@ namespace Discord.WebSocket private bool _nsfw; /// public bool IsNsfw => _nsfw; - + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } /// public string Mention => MentionUtils.MentionChannel(Id); /// @@ -76,6 +77,11 @@ namespace Discord.WebSocket Topic = model.Topic.GetValueOrDefault(); SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet? _nsfw = model.Nsfw.GetValueOrDefault(); + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// @@ -128,7 +134,7 @@ namespace Discord.WebSocket #region Messages /// - public SocketMessage GetCachedMessage(ulong id) + public virtual SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); /// /// Gets a message from this message channel. @@ -143,7 +149,7 @@ namespace Discord.WebSocket /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - public async Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual async Task GetMessageAsync(ulong id, RequestOptions options = null) { IMessage msg = _messages?.Get(id); if (msg == null) @@ -163,7 +169,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -179,7 +185,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -195,25 +201,25 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); /// - public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); /// - public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); /// - public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -221,7 +227,7 @@ namespace Discord.WebSocket /// /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -230,7 +236,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -239,7 +245,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -248,7 +254,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -256,28 +262,28 @@ namespace Discord.WebSocket messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); internal void AddMessage(SocketMessage msg) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 00003d4ed..5fc99c3f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -14,33 +15,21 @@ namespace Discord.WebSocket /// Represents a WebSocket-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel + public class SocketVoiceChannel : SocketTextChannel, IVoiceChannel, ISocketAudioChannel { #region SocketVoiceChannel - /// - public int Bitrate { get; private set; } - /// - public int? UserLimit { get; private set; } - /// - public string RTCRegion { get; private set; } - - /// - public ulong? CategoryId { get; private set; } /// - /// Gets the parent (category) channel of this channel. + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. /// - /// - /// A category channel representing the parent of this channel; null if none is set. - /// - public ICategoryChannel Category - => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + public virtual bool IsTextInVoice + => Guild.Features.HasTextInVoice; /// - public string Mention => MentionUtils.MentionChannel(Id); - + public int Bitrate { get; private set; } + /// + public int? UserLimit { get; private set; } /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + public string RTCRegion { get; private set; } /// /// Gets a collection of users that are currently connected to this voice channel. @@ -48,7 +37,7 @@ namespace Discord.WebSocket /// /// A read-only collection of users that are currently connected to this voice channel. /// - public override IReadOnlyCollection Users + public IReadOnlyCollection ConnectedUsers => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) @@ -65,7 +54,6 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - CategoryId = model.CategoryId; Bitrate = model.Bitrate.Value; UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; RTCRegion = model.RTCRegion.GetValueOrDefault(null); @@ -99,28 +87,215 @@ namespace Discord.WebSocket return user; return null; } -#endregion - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// Cannot create threads in voice channels. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Voice channels cannot contain threads."); + + /// Cannot modify text channel properties for voice channels. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties for voice channels."); + + #endregion + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override SocketMessage GetCachedMessage(ulong id) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessage(id); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessage, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessageId, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; - #endregion #region IGuildChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 49d2cd3bd..e12f3d1ef 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1291,7 +1291,6 @@ namespace Discord.WebSocket /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. @@ -1781,7 +1780,7 @@ namespace Discord.WebSocket /// ulong? IGuild.AFKChannelId => AFKChannelId; /// - IAudioClient IGuild.AudioClient => null; + IAudioClient IGuild.AudioClient => AudioClient; /// bool IGuild.Available => true; /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index cfbd3096d..647544b48 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -174,6 +174,91 @@ namespace Discord.WebSocket HasResponded = true; } + public async Task UpdateAsync(Action func, RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed."); + } + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : false; + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0; + + if (!hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue) + { + var allowedMentions = args.AllowedMentions.Value; + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) + && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) + && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + /// public override async Task FollowupAsync( string text = null, diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 36eba0cd1..8f27b65f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -36,6 +36,12 @@ namespace Discord.WebSocket /// public bool IsDefaultPermission { get; private set; } + /// + public bool IsEnabledInDm { get; private set; } + + /// + public GuildPermissions DefaultMemberPermissions { get; private set; } + /// /// Gets a collection of s for this command. /// @@ -86,6 +92,9 @@ namespace Discord.WebSocket Options = model.Options.IsSpecified ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 6668426e1..3cd67beb5 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -182,7 +182,8 @@ namespace Discord.WebSocket { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } diff --git a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs index 4cd9ef264..a2a101839 100644 --- a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs +++ b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs @@ -1,11 +1,13 @@ using Discord.WebSocket; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Discord.Interactions { /// /// Represents a Web-Socket based context of an . /// - public class SocketInteractionContext : IInteractionContext + public class SocketInteractionContext : IInteractionContext, IRouteMatchContainer where TInteraction : SocketInteraction { /// @@ -36,6 +38,9 @@ namespace Discord.Interactions /// public TInteraction Interaction { get; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } + /// /// Initializes a new . /// @@ -50,6 +55,13 @@ namespace Discord.Interactions Interaction = interaction; } + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; + // IInteractionContext /// IDiscordClient IInteractionContext.Client => Client; diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index df920b7dc..1e3c3f7f8 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -6,6 +6,8 @@ Discord.Webhook A core Discord.Net library containing the Webhook client and models. net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 405100f89..556338956 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -88,8 +88,8 @@ namespace Discord.Webhook /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags, threadId); /// /// Modifies a message posted using this webhook. @@ -103,8 +103,8 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous modification operation. /// - public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) - => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null, ulong? threadId = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options, threadId); /// /// Deletes a message posted using this webhook. @@ -117,43 +117,43 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous deletion operation. /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => WebhookClientHelper.DeleteMessageAsync(this, messageId, options); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null, ulong ? threadId = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, options, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components, flags); + allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components, flags); + avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options, flags); + avatarUrl, allowedMentions, components, options, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options, flags); + allowedMentions, components, options, flags, threadId); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 0a974a9d9..8ad74e7e7 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,8 +21,8 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, - AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags, ulong? threadId = null) { var args = new CreateWebhookMessageParams { @@ -44,12 +44,13 @@ namespace Discord.Webhook if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); - - var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options, threadId: threadId).ConfigureAwait(false); return model.Id; } + public static async Task ModifyMessageAsync(DiscordWebhookClient client, ulong messageId, - Action func, RequestOptions options) + Action func, RequestOptions options, ulong? threadId) { var args = new WebhookMessageProperties(); func(args); @@ -94,35 +95,35 @@ namespace Discord.Webhook Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, }; - await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options) + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) .ConfigureAwait(false); } - public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options) + public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options, ulong? threadId) { - await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); + await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options, threadId).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, - bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None, ulong? threadId = null) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId).ConfigureAwait(false); } public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, - MessageComponent components, MessageFlags flags) - => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, MessageFlags flags, ulong? threadId) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, - MessageComponent components, RequestOptions options, MessageFlags flags) - => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, RequestOptions options, MessageFlags flags, ulong? threadId) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static async Task SendFilesAsync(DiscordWebhookClient client, IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, - MessageFlags flags) + MessageFlags flags, ulong? threadId) { embeds ??= Array.Empty(); @@ -164,7 +165,7 @@ namespace Discord.Webhook MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Flags = flags }; - var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options, threadId).ConfigureAwait(false); return msg.Id; } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index d79e9a24a..3985536f4 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,57 +2,57 @@ Discord.Net - 3.5.0$suffix$ + 3.6.1$suffix$ Discord.Net Discord.Net Contributors foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp - https://github.com/RogueException/Discord.Net + https://github.com/discord-net/Discord.Net http://opensource.org/licenses/MIT false - https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - \ No newline at end of file + diff --git a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj index 0f399ab68..7b8257bfb 100644 --- a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj +++ b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs new file mode 100644 index 000000000..96b33b141 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs @@ -0,0 +1,53 @@ +using Discord.API; +using Discord.API.Rest; +using Discord.Net; +using Discord.Rest; +using FluentAssertions; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Discord; + +[CollectionDefinition(nameof(DiscordRestApiClientTests), DisableParallelization = true)] +public class DiscordRestApiClientTests : IClassFixture, IAsyncDisposable +{ + private readonly DiscordRestApiClient _apiClient; + private readonly IGuild _guild; + private readonly ITextChannel _channel; + + public DiscordRestApiClientTests(RestGuildFixture guildFixture) + { + _guild = guildFixture.Guild; + _apiClient = guildFixture.Client.ApiClient; + _channel = _guild.CreateTextChannelAsync("testChannel").Result; + } + + public async ValueTask DisposeAsync() + { + await _channel.DeleteAsync(); + } + + [Fact] + public async Task UploadFile_WithMaximumSize_DontThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild); + using var stream = new MemoryStream(new byte[fileSize]); + + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + } + + [Fact] + public async Task UploadFile_WithOverSize_ThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild) + 1; + using var stream = new MemoryStream(new byte[fileSize]); + + Func upload = async () => + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + + await upload.Should().ThrowExactlyAsync() + .Where(e => e.DiscordCode == DiscordErrorCode.RequestEntityTooLarge); + } +} diff --git a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj index ec06c3c3d..087a64d83 100644 --- a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj +++ b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj @@ -12,7 +12,9 @@ + + all diff --git a/test/Discord.Net.Tests.Unit/GuildHelperTests.cs b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs new file mode 100644 index 000000000..c68f415fe --- /dev/null +++ b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs @@ -0,0 +1,25 @@ +using Discord.Rest; +using FluentAssertions; +using Moq; +using System; +using Xunit; + +namespace Discord; + +public class GuildHelperTests +{ + [Theory] + [InlineData(PremiumTier.None, 8)] + [InlineData(PremiumTier.Tier1, 8)] + [InlineData(PremiumTier.Tier2, 50)] + [InlineData(PremiumTier.Tier3, 100)] + public void GetUploadLimit(PremiumTier tier, ulong factor) + { + var guild = Mock.Of(g => g.PremiumTier == tier); + var expected = factor * (ulong)Math.Pow(2, 20); + + var actual = GuildHelper.GetUploadLimit(guild); + + actual.Should().Be(expected); + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 0dfcab7a5..ab1d3e534 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -10,6 +10,8 @@ namespace Discord { public bool IsNsfw => throw new NotImplementedException(); + public ThreadArchiveDuration DefaultArchiveDuration => throw new NotImplementedException(); + public string Topic => throw new NotImplementedException(); public int SlowModeInterval => throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 533b1b1b5..fdbdeda5e 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; using Discord.Audio; @@ -12,8 +13,6 @@ namespace Discord public int? UserLimit => throw new NotImplementedException(); - public string Mention => throw new NotImplementedException(); - public ulong? CategoryId => throw new NotImplementedException(); public int Position => throw new NotImplementedException(); @@ -24,116 +23,53 @@ namespace Discord public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + public string RTCRegion => throw new NotImplementedException(); + public string Name => throw new NotImplementedException(); public DateTimeOffset CreatedAt => throw new NotImplementedException(); - public ulong Id => throw new NotImplementedException(); - - public string RTCRegion => throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } + public ulong Id => throw new NotImplementedException(); - public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) - { - throw new NotImplementedException(); - } + public string Mention => throw new NotImplementedException(); - public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - { - throw new NotImplementedException(); - } - public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException(); + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); - public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - - public Task DeleteAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task DisconnectAsync() - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options) - { - throw new NotImplementedException(); - } - - public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task> GetInvitesAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - throw new NotImplementedException(); - } - - public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task SyncPermissionsAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } - - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } + public Task CreateInviteToStreamAsync(IUser user, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + public Task DisconnectAsync() => throw new NotImplementedException(); + public IDisposable EnterTypingState(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetInvitesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task TriggerTypingAsync(RequestOptions options = null) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotImplementedException(); } }