| @@ -1 +1,3 @@ | |||
| github: quinchs | |||
| open_collective: discordnet | |||
| custom: https://paypal.me/quinchs | |||
| @@ -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 | |||
| @@ -1,4 +1,55 @@ | |||
| # Changelog | |||
| ## [3.7.2] - 2022-06-02 | |||
| ### Added | |||
| - #2328 Add method overloads to InteractionService (0fad3e8) | |||
| - #2336 Add support for attachments on interaction response type 7 (35db22e) | |||
| - #2338 AddOptions no longer has an uneeded restriction, added AddOptions to SlashCommandOptionBuilder (3a37f89) | |||
| ### Fixed | |||
| - #2342 Disable TIV restrictions for rollout of TIV (7adf516) | |||
| ## [3.7.1] - 2022-05-27 | |||
| ### Added | |||
| - #2325 Add missing interaction properties (d3a693a) | |||
| - #2330 Add better call control in ParseHttpInteraction (a890de9) | |||
| ### Fixed | |||
| - #2329 Voice perms not retaining text perms. (712a4ae) | |||
| - #2331 NRE with Cacheable.DownloadAsync() (e1f9b76) | |||
| ## [3.7.0] - 2022-05-24 | |||
| ### Added | |||
| - #2269 Text-In-Voice (23656e8) | |||
| - #2281 Optional API calling to RestInteraction (a24dde4) | |||
| - #2283 Support FailIfNotExists on MessageReference (0ec8938) | |||
| - #2284 Add Parse & TryParse to EmbedBuilder & Add ToJsonString extension (cea59b5) | |||
| - #2289 Add UpdateAsync to SocketModal (b333de2) | |||
| - #2291 Webhook support for threads (b0a3b65) | |||
| - #2295 Add DefaultArchiveDuration to ITextChannel (1f01881) | |||
| - #2296 Add `.With` methods to ActionRowBuilder (13ccc7c) | |||
| - #2307 Add Nullable ComponentTypeConverter and TypeReader (6fbd396) | |||
| - #2316 Forum channels (7a07fd6) | |||
| ### Fixed | |||
| - #2290 Possible NRE in Sanitize (20ffa64) | |||
| - #2293 Application commands are disabled to everyone except admins by default (b465d60) | |||
| - #2299 Close-stage bucketId being null (725d255) | |||
| - #2313 Upload file size limit being incorrectly calculated (54a5af7) | |||
| - #2319 Use `IDiscordClient.GetUserAsync` impl in `DiscordSocketClient` (f47f319) | |||
| - #2320 NRE with bot scope and user parameters (88f6168) | |||
| ## [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 | |||
| @@ -1,12 +1,12 @@ | |||
| <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
| <PropertyGroup> | |||
| <VersionPrefix>3.6.0</VersionPrefix> | |||
| <VersionPrefix>3.7.2</VersionPrefix> | |||
| <LangVersion>latest</LangVersion> | |||
| <Authors>Discord.Net Contributors</Authors> | |||
| <PackageTags>discord;discordapp</PackageTags> | |||
| <PackageProjectUrl>https://github.com/Discord-Net/Discord.Net</PackageProjectUrl> | |||
| <PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl> | |||
| <PackageIconUrl>https://github.com/Discord-Net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</PackageIconUrl> | |||
| <PackageLicenseExpression>MIT</PackageLicenseExpression> | |||
| <PackageIcon>PackageLogo.png</PackageIcon> | |||
| <RepositoryType>git</RepositoryType> | |||
| <RepositoryUrl>git://github.com/Discord-Net/Discord.Net</RepositoryUrl> | |||
| </PropertyGroup> | |||
| @@ -23,4 +23,7 @@ | |||
| <WarningsAsErrors>true</WarningsAsErrors> | |||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <None Include="../../docs/marketing/logo/PackageLogo.png" Pack="true" PackagePath=""/> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -17,7 +17,7 @@ | |||
| <img src="https://discord.com/api/guilds/848176216011046962/widget.png" alt="Discord"> | |||
| </a> | |||
| </p> | |||
| 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 | |||
| @@ -60,7 +60,7 @@ | |||
| "overwrite": "_overwrites/**/**.md", | |||
| "globalMetadata": { | |||
| "_appTitle": "Discord.Net Documentation", | |||
| "_appFooter": "Discord.Net (c) 2015-2022 3.6.0", | |||
| "_appFooter": "Discord.Net (c) 2015-2022 3.7.2", | |||
| "_enableSearch": true, | |||
| "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", | |||
| "_appFaviconPath": "favicon.ico" | |||
| @@ -99,7 +99,7 @@ When we run the command, our modal should pop up: | |||
| ### Respond to modals | |||
| > [!WARNING] | |||
| > Modals can not be sent when respoding to a modal. | |||
| > Modals can not be sent when responding to a modal. | |||
| Once a user has submitted the modal, we need to let everyone know what | |||
| their favorite food is. We can start by hooking a task to the client's | |||
| @@ -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. | |||
| @@ -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` | |||
| @@ -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<AutocompletionResult> GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) | |||
| { | |||
| // Create a collection with suggestions for autocomplete | |||
| IEnumerable<AutocompleteResult> results = new[] | |||
| { | |||
| new AutocompleteResult("Name1", "value111"), | |||
| new AutocompleteResult("Name2", "value2") | |||
| }; | |||
| // max - 25 suggestions at a time (API limit) | |||
| return AutocompletionResult.FromSuccess(results.Take(25)); | |||
| } | |||
| } | |||
| @@ -1,9 +1,21 @@ | |||
| [AutocompleteCommand("parameter_name", "command_name")] | |||
| public async Task Autocomplete() | |||
| { | |||
| IEnumerable<AutocompleteResult> results; | |||
| string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString(); | |||
| ... | |||
| IEnumerable<AutocompleteResult> 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}"); | |||
| @@ -1,12 +1,12 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| <PackageReference Include="Discord.Net.WebSocket" Version="3.6.1"/> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -2,7 +2,7 @@ | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| <RootNamespace>InteractionFramework</RootNamespace> | |||
| <StartupObject></StartupObject> | |||
| </PropertyGroup> | |||
| @@ -13,13 +13,7 @@ | |||
| <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> | |||
| <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> | |||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| <PackageReference Include="Discord.Net.Interactions" Version="3.6.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -2,18 +2,13 @@ | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| <RootNamespace>ShardedClient</RootNamespace> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| <PackageReference Include="Discord.Net" Version="3.6.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -2,17 +2,14 @@ | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| <RootNamespace>TextCommandFramework</RootNamespace> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||
| <ProjectReference Include="..\..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| <PackageReference Include="Discord.Net.Commands" Version="3.6.1" /> | |||
| <PackageReference Include="Discord.Net.Websocket" Version="3.6.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -2,12 +2,12 @@ | |||
| <PropertyGroup> | |||
| <OutputType>Exe</OutputType> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| <RootNamespace>WebHookClient</RootNamespace> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\..\src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" /> | |||
| <PackageReference Include="Discord.Net.Webhook" Version="3.6.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -7,6 +7,8 @@ | |||
| <Description>A Discord.Net extension adding support for bot commands.</Description> | |||
| <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <WarningLevel>5</WarningLevel> | |||
| <TreatWarningsAsErrors>True</TreatWarningsAsErrors> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| @@ -1,4 +1,4 @@ | |||
| using System; | |||
| using System; | |||
| namespace Discord.Commands | |||
| { | |||
| @@ -12,7 +12,7 @@ namespace Discord.Commands | |||
| /// <summary> | |||
| /// Gets on which pipeline stage the command may have matched or failed. | |||
| /// </summary> | |||
| public IResult? Pipeline { get; } | |||
| public IResult Pipeline { get; } | |||
| /// <inheritdoc /> | |||
| public CommandError? Error { get; } | |||
| @@ -21,7 +21,7 @@ namespace Discord.Commands | |||
| /// <inheritdoc /> | |||
| 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; | |||
| @@ -7,6 +7,8 @@ | |||
| <Description>The core components for the Discord.Net library.</Description> | |||
| <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <WarningLevel>5</WarningLevel> | |||
| <TreatWarningsAsErrors>True</TreatWarningsAsErrors> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> | |||
| @@ -132,6 +132,16 @@ namespace Discord | |||
| /// </returns> | |||
| public const int MaxAuditLogEntriesPerBatch = 100; | |||
| /// <summary> | |||
| /// Returns the max number of stickers that can be sent with a message. | |||
| /// </summary> | |||
| public const int MaxStickersPerMessage = 3; | |||
| /// <summary> | |||
| /// Returns the max number of embeds that can be sent with a message. | |||
| /// </summary> | |||
| public const int MaxEmbedsPerMessage = 10; | |||
| /// <summary> | |||
| /// Gets or sets how a request should act in the case of an error, by default. | |||
| /// </summary> | |||
| @@ -152,6 +152,7 @@ namespace Discord | |||
| InvalidMessageType = 50068, | |||
| PaymentSourceRequiredForGift = 50070, | |||
| CannotDeleteRequiredCommunityChannel = 50074, | |||
| CannotEditStickersWithinAMessage = 50080, | |||
| InvalidSticker = 50081, | |||
| CannotExecuteOnArchivedThread = 50083, | |||
| InvalidThreadNotificationSettings = 50084, | |||
| @@ -164,6 +165,7 @@ namespace Discord | |||
| #endregion | |||
| #region 2FA (60XXX) | |||
| MissingPermissionToSendThisSticker = 50600, | |||
| Requires2FA = 60003, | |||
| #endregion | |||
| @@ -26,6 +26,8 @@ namespace Discord | |||
| /// <summary> The channel is a stage voice channel. </summary> | |||
| Stage = 13, | |||
| /// <summary> The channel is a guild directory used in hub servers. (Unreleased)</summary> | |||
| GuildDirectory = 14 | |||
| GuildDirectory = 14, | |||
| /// <summary> The channel is a forum channel containing multiple threads. </summary> | |||
| Forum = 15 | |||
| } | |||
| } | |||
| @@ -0,0 +1,216 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public interface IForumChannel : IGuildChannel, IMentionable | |||
| { | |||
| /// <summary> | |||
| /// Gets a value that indicates whether the channel is NSFW. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// <c>true</c> if the channel has the NSFW flag enabled; otherwise <c>false</c>. | |||
| /// </returns> | |||
| bool IsNsfw { get; } | |||
| /// <summary> | |||
| /// Gets the current topic for this text channel. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A string representing the topic set in the channel; <c>null</c> if none is set. | |||
| /// </returns> | |||
| string Topic { get; } | |||
| /// <summary> | |||
| /// Gets the default archive duration for a newly created post. | |||
| /// </summary> | |||
| ThreadArchiveDuration DefaultAutoArchiveDuration { get; } | |||
| /// <summary> | |||
| /// Gets a collection of tags inside of this forum channel. | |||
| /// </summary> | |||
| IReadOnlyCollection<ForumTag> Tags { get; } | |||
| /// <summary> | |||
| /// Creates a new post (thread) within the forum. | |||
| /// </summary> | |||
| /// <param name="title">The title of the post.</param> | |||
| /// <param name="archiveDuration">The archive duration of the post.</param> | |||
| /// <param name="slowmode">The slowmode for the posts thread.</param> | |||
| /// <param name="text">The message to be sent.</param> | |||
| /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <param name="allowedMentions"> | |||
| /// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>. | |||
| /// If <c>null</c>, all mentioned roles and users will be notified. | |||
| /// </param> | |||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | |||
| /// <param name="stickers">A collection of stickers to send with the message.</param> | |||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. | |||
| /// </returns> | |||
| Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, | |||
| string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | |||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||
| /// <summary> | |||
| /// Creates a new post (thread) within the forum. | |||
| /// </summary> | |||
| /// <param name="title">The title of the post.</param> | |||
| /// <param name="archiveDuration">The archive duration of the post.</param> | |||
| /// <param name="slowmode">The slowmode for the posts thread.</param> | |||
| /// <param name="filePath">The file path of the file.</param> | |||
| /// <param name="text">The message to be sent.</param> | |||
| /// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> | |||
| /// <param name="allowedMentions"> | |||
| /// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>. | |||
| /// If <c>null</c>, all mentioned roles and users will be notified. | |||
| /// </param> | |||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | |||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | |||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. | |||
| /// </returns> | |||
| Task<IThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | |||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | |||
| ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||
| /// <summary> | |||
| /// Creates a new post (thread) within the forum. | |||
| /// </summary> | |||
| /// <param name="title">The title of the post.</param> | |||
| /// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param> | |||
| /// <param name="filename">The name of the attachment.</param> | |||
| /// <param name="archiveDuration">The archive duration of the post.</param> | |||
| /// <param name="slowmode">The slowmode for the posts thread.</param> | |||
| /// <param name="text">The message to be sent.</param> | |||
| /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param> | |||
| /// <param name="allowedMentions"> | |||
| /// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>. | |||
| /// If <c>null</c>, all mentioned roles and users will be notified. | |||
| /// </param> | |||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | |||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | |||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. | |||
| /// </returns> | |||
| public Task<IThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | |||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | |||
| ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None); | |||
| /// <summary> | |||
| /// Creates a new post (thread) within the forum. | |||
| /// </summary> | |||
| /// <param name="title">The title of the post.</param> | |||
| /// <param name="attachment">The attachment containing the file and description.</param> | |||
| /// <param name="archiveDuration">The archive duration of the post.</param> | |||
| /// <param name="slowmode">The slowmode for the posts thread.</param> | |||
| /// <param name="text">The message to be sent.</param> | |||
| /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <param name="allowedMentions"> | |||
| /// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>. | |||
| /// If <c>null</c>, all mentioned roles and users will be notified. | |||
| /// </param> | |||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | |||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | |||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. | |||
| /// </returns> | |||
| public Task<IThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | |||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||
| /// <summary> | |||
| /// Creates a new post (thread) within the forum. | |||
| /// </summary> | |||
| /// <param name="title">The title of the post.</param> | |||
| /// <param name="attachments">A collection of attachments to upload.</param> | |||
| /// <param name="archiveDuration">The archive duration of the post.</param> | |||
| /// <param name="slowmode">The slowmode for the posts thread.</param> | |||
| /// <param name="text">The message to be sent.</param> | |||
| /// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <param name="allowedMentions"> | |||
| /// Specifies if notifications are sent for mentioned users and roles in the message <paramref name="text"/>. | |||
| /// If <c>null</c>, all mentioned roles and users will be notified. | |||
| /// </param> | |||
| /// <param name="components">The message components to be included with this message. Used for interactions.</param> | |||
| /// <param name="stickers">A collection of stickers to send with the file.</param> | |||
| /// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param> | |||
| /// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. | |||
| /// </returns> | |||
| public Task<IThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | |||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); | |||
| /// <summary> | |||
| /// Gets a collection of active threads within this forum channel. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||
| /// a collection of active threads. | |||
| /// </returns> | |||
| Task<IReadOnlyCollection<IThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of publicly archived threads within this forum channel. | |||
| /// </summary> | |||
| /// <param name="limit">The optional limit of how many to get.</param> | |||
| /// <param name="before">The optional date to return threads created before this timestamp.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||
| /// a collection of publicly archived threads. | |||
| /// </returns> | |||
| Task<IReadOnlyCollection<IThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of privately archived threads within this forum channel. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// The bot requires the <see cref="GuildPermission.ManageThreads"/> permission in order to execute this request. | |||
| /// </remarks> | |||
| /// <param name="limit">The optional limit of how many to get.</param> | |||
| /// <param name="before">The optional date to return threads created before this timestamp.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||
| /// a collection of privately archived threads. | |||
| /// </returns> | |||
| Task<IReadOnlyCollection<IThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a collection of privately archived threads that the current bot has joined within this forum channel. | |||
| /// </summary> | |||
| /// <param name="limit">The optional limit of how many to get.</param> | |||
| /// <param name="before">The optional date to return threads created before this timestamp.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||
| /// a collection of privately archived threads. | |||
| /// </returns> | |||
| Task<IReadOnlyCollection<IThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -35,6 +35,17 @@ namespace Discord | |||
| /// </returns> | |||
| int SlowModeInterval { get; } | |||
| /// <summary> | |||
| /// Gets the default auto-archive duration for client-created threads in this channel. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// The value of this property does not affect API thread creation, it will not respect this value. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// The default auto-archive duration for thread creation in this channel. | |||
| /// </returns> | |||
| ThreadArchiveDuration DefaultArchiveDuration { get; } | |||
| /// <summary> | |||
| /// Bulk-deletes multiple messages. | |||
| /// </summary> | |||
| @@ -6,7 +6,7 @@ namespace Discord | |||
| /// <summary> | |||
| /// Represents a generic voice channel in a guild. | |||
| /// </summary> | |||
| public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable | |||
| public interface IVoiceChannel : IMessageChannel, INestedChannel, IAudioChannel, IMentionable | |||
| { | |||
| /// <summary> | |||
| /// Gets the bit-rate that the clients in this voice channel are requested to use. | |||
| @@ -0,0 +1,42 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// A struct representing a forum channel tag. | |||
| /// </summary> | |||
| public struct ForumTag | |||
| { | |||
| /// <summary> | |||
| /// Gets the Id of the tag. | |||
| /// </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> | |||
| /// Gets the name of the tag. | |||
| /// </summary> | |||
| public string Name { get; } | |||
| /// <summary> | |||
| /// Gets the emoji of the tag or <see langword="null"/> if none is set. | |||
| /// </summary> | |||
| public IEmote Emoji { get; } | |||
| internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName) | |||
| { | |||
| if (emojiId.HasValue && emojiId.Value != 0) | |||
| Emoji = new Emote(emojiId.Value, emojiName, false); | |||
| else if (emojiName != null) | |||
| Emoji = new Emoji(name); | |||
| else | |||
| Emoji = null; | |||
| Id = id; | |||
| Name = name; | |||
| } | |||
| } | |||
| } | |||
| @@ -1173,7 +1173,6 @@ namespace Discord | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="coverImage">The optional banner image for the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| @@ -89,7 +89,7 @@ namespace Discord | |||
| /// Gets this events banner image url. | |||
| /// </summary> | |||
| /// <param name="format">The format to return.</param> | |||
| /// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048. | |||
| /// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param> | |||
| /// <returns>The cover images url.</returns> | |||
| string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); | |||
| @@ -56,7 +56,7 @@ namespace Discord | |||
| Number = 10, | |||
| /// <summary> | |||
| /// A <see cref="Discord.Attachment"/>. | |||
| /// A <see cref="IAttachment"/>. | |||
| /// </summary> | |||
| Attachment = 11 | |||
| } | |||
| @@ -52,10 +52,13 @@ namespace Discord | |||
| /// <summary> | |||
| /// Gets the preferred locale of the invoking User. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property returns <see langword="null"/> if the interaction is a REST ping interaction. | |||
| /// </remarks> | |||
| string UserLocale { get; } | |||
| /// <summary> | |||
| /// Gets the preferred locale of the guild this interaction was executed in. <see cref="null"/> if not executed in a guild. | |||
| /// Gets the preferred locale of the guild this interaction was executed in. <see langword="null"/> if not executed in a guild. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Non-community guilds (With no locale setting available) will have en-US as the default value sent by Discord. | |||
| @@ -67,6 +70,27 @@ namespace Discord | |||
| /// </summary> | |||
| bool IsDMInteraction { get; } | |||
| /// <summary> | |||
| /// Gets the ID of the channel this interaction was executed in. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property returns <see langword="null"/> if the interaction is a REST ping interaction. | |||
| /// </remarks> | |||
| ulong? ChannelId { get; } | |||
| /// <summary> | |||
| /// Gets the ID of the guild this interaction was executed in. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property returns <see langword="null"/> if the interaction was not executed in a guild. | |||
| /// </remarks> | |||
| ulong? GuildId { get; } | |||
| /// <summary> | |||
| /// Gets the ID of the application this interaction is for. | |||
| /// </summary> | |||
| ulong ApplicationId { get; } | |||
| /// <summary> | |||
| /// Responds to an Interaction with type <see cref="InteractionResponseType.ChannelMessageWithSource"/>. | |||
| /// </summary> | |||
| @@ -195,7 +195,7 @@ namespace Discord | |||
| /// </summary> | |||
| /// <param name="button">The button to add.</param> | |||
| /// <param name="row">The row to add the button.</param> | |||
| /// <exception cref="InvalidOperationException">There is no more row to add a menu.</exception> | |||
| /// <exception cref="InvalidOperationException">There is no more row to add a button.</exception> | |||
| /// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception> | |||
| /// <returns>The current builder.</returns> | |||
| public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) | |||
| @@ -348,6 +348,100 @@ namespace Discord | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="customId">The custom id of the menu.</param> | |||
| /// <param name="options">The options of the menu.</param> | |||
| /// <param name="placeholder">The placeholder of the menu.</param> | |||
| /// <param name="minValues">The min values of the placeholder.</param> | |||
| /// <param name="maxValues">The max values of the placeholder.</param> | |||
| /// <param name="disabled">Whether or not the menu is disabled.</param> | |||
| /// <returns>The current builder.</returns> | |||
| public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> 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)); | |||
| } | |||
| /// <summary> | |||
| /// Adds a <see cref="SelectMenuBuilder"/> to the <see cref="ActionRowBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="menu">The menu to add.</param> | |||
| /// <exception cref="InvalidOperationException">A Select Menu cannot exist in a pre-occupied ActionRow.</exception> | |||
| /// <returns>The current builder.</returns> | |||
| 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; | |||
| } | |||
| /// <summary> | |||
| /// Adds a <see cref="ButtonBuilder"/> with specified parameters to the <see cref="ActionRowBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="label">The label text for the newly added button.</param> | |||
| /// <param name="style">The style of this newly added button.</param> | |||
| /// <param name="emote">A <see cref="IEmote"/> to be used with this button.</param> | |||
| /// <param name="customId">The custom id of the newly added button.</param> | |||
| /// <param name="url">A URL to be used only if the <see cref="ButtonStyle"/> is a Link.</param> | |||
| /// <param name="disabled">Whether or not the newly created button is disabled.</param> | |||
| /// <returns>The current builder.</returns> | |||
| 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); | |||
| } | |||
| /// <summary> | |||
| /// Adds a <see cref="ButtonBuilder"/> to the <see cref="ActionRowBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="button">The button to add.</param> | |||
| /// <exception cref="InvalidOperationException">Components count reached <see cref="MaxChildCount"/>.</exception> | |||
| /// <exception cref="InvalidOperationException">A button cannot be added to a row with a SelectMenu.</exception> | |||
| /// <returns>The current builder.</returns> | |||
| 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; | |||
| } | |||
| /// <summary> | |||
| /// Builds the current builder to a <see cref="ActionRowComponent"/> that can be used within a <see cref="ComponentBuilder"/> | |||
| /// </summary> | |||
| @@ -1194,9 +1288,9 @@ namespace Discord | |||
| /// <summary> | |||
| /// Gets or sets the default value of the text input. | |||
| /// </summary> | |||
| /// <exception cref="ArgumentOutOfRangeException"><see cref="Value.Length"/> is less than 0.</exception> | |||
| /// <exception cref="ArgumentOutOfRangeException"><see cref="Value"/>.Length is less than 0.</exception> | |||
| /// <exception cref="ArgumentOutOfRangeException"> | |||
| /// <see cref="Value.Length"/> is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>. | |||
| /// <see cref="Value"/>.Length is greater than <see cref="LargestMaxLength"/> or <see cref="MaxLength"/>. | |||
| /// </exception> | |||
| public string Value | |||
| { | |||
| @@ -1227,7 +1321,7 @@ namespace Discord | |||
| /// <param name="minLength">The text input's minimum length.</param> | |||
| /// <param name="maxLength">The text input's maximum length.</param> | |||
| /// <param name="required">The text input's required value.</param> | |||
| 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; | |||
| } | |||
| /// <summary> | |||
| /// Sets the value of the current builder. | |||
| /// </summary> | |||
| @@ -1306,18 +1400,18 @@ namespace Discord | |||
| /// <summary> | |||
| /// Sets the minimum length of the current builder. | |||
| /// </summary> | |||
| /// <param name="placeholder">The value to set.</param> | |||
| /// <param name="minLength">The value to set.</param> | |||
| /// <returns>The current builder. </returns> | |||
| public TextInputBuilder WithMinLength(int minLength) | |||
| { | |||
| MinLength = minLength; | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the maximum length of the current builder. | |||
| /// </summary> | |||
| /// <param name="placeholder">The value to set.</param> | |||
| /// <param name="maxLength">The value to set.</param> | |||
| /// <returns>The current builder. </returns> | |||
| public TextInputBuilder WithMaxLength(int maxLength) | |||
| { | |||
| @@ -64,18 +64,18 @@ namespace Discord | |||
| /// <summary> | |||
| /// Sets the custom id of the current modal. | |||
| /// </summary> | |||
| /// <param name="title">The value to set the custom id to.</param> | |||
| /// <param name="customId">The value to set the custom id to.</param> | |||
| /// <returns>The current builder.</returns> | |||
| public ModalBuilder WithCustomId(string customId) | |||
| { | |||
| CustomId = customId; | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Adds a component to the current builder. | |||
| /// </summary> | |||
| /// <param name="title">The component to add.</param> | |||
| /// <param name="component">The component to add.</param> | |||
| /// <returns>The current builder.</returns> | |||
| public ModalBuilder AddTextInput(TextInputBuilder component) | |||
| { | |||
| @@ -213,7 +213,7 @@ namespace Discord | |||
| /// Adds a <see cref="TextInputBuilder"/> to the <see cref="ModalComponentBuilder"/> at the specific row. | |||
| /// If the row cannot accept the component then it will add it to a row that can. | |||
| /// </summary> | |||
| /// <param name="text">The <see cref="TextInputBuilder"> to add.</param> | |||
| /// <param name="text">The <see cref="TextInputBuilder"/> to add.</param> | |||
| /// <param name="row">The row to add the text input.</param> | |||
| /// <exception cref="InvalidOperationException">There are no more rows to add a text input to.</exception> | |||
| /// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception> | |||
| @@ -255,9 +255,6 @@ namespace Discord | |||
| if (options == null) | |||
| throw new ArgumentNullException(nameof(options), "Options cannot be null!"); | |||
| if (options.Length == 0) | |||
| throw new ArgumentException("Options cannot be empty!", nameof(options)); | |||
| Options ??= new List<SlashCommandOptionBuilder>(); | |||
| if (Options.Count + options.Length > MaxOptionsCount) | |||
| @@ -409,7 +406,7 @@ namespace Discord | |||
| MinValue = MinValue, | |||
| MaxValue = MaxValue | |||
| }; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Adds an option to the current slash command. | |||
| @@ -477,6 +474,26 @@ namespace Discord | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Adds a collection of options to the current option. | |||
| /// </summary> | |||
| /// <param name="options">The collection of options to add.</param> | |||
| /// <returns>The current builder.</returns> | |||
| public SlashCommandOptionBuilder AddOptions(params SlashCommandOptionBuilder[] options) | |||
| { | |||
| if (options == null) | |||
| throw new ArgumentNullException(nameof(options), "Options cannot be null!"); | |||
| if ((Options?.Count ?? 0) + options.Length > SlashCommandBuilder.MaxOptionsCount) | |||
| throw new ArgumentOutOfRangeException(nameof(options), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); | |||
| foreach (var option in options) | |||
| Preconditions.Options(option.Name, option.Description); | |||
| Options.AddRange(options); | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Adds a choice to the current option. | |||
| /// </summary> | |||
| @@ -640,7 +657,7 @@ namespace Discord | |||
| MinValue = value; | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Sets the current builders max value field. | |||
| /// </summary> | |||
| @@ -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 | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Tries to parse a string into an <see cref="EmbedBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="json">The json string to parse.</param> | |||
| /// <param name="builder">The <see cref="EmbedBuilder"/> with populated values. An empty instance if method returns <see langword="false"/>.</param> | |||
| /// <returns><see langword="true"/> if <paramref name="json"/> was succesfully parsed. <see langword="false"/> if not.</returns> | |||
| public static bool TryParse(string json, out EmbedBuilder builder) | |||
| { | |||
| builder = new EmbedBuilder(); | |||
| try | |||
| { | |||
| var model = JsonConvert.DeserializeObject<Embed>(json); | |||
| if (model is not null) | |||
| { | |||
| builder = model.ToEmbedBuilder(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| catch | |||
| { | |||
| return false; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Parses a string into an <see cref="EmbedBuilder"/>. | |||
| /// </summary> | |||
| /// <param name="json">The json string to parse.</param> | |||
| /// <returns>An <see cref="EmbedBuilder"/> with populated values from the passed <paramref name="json"/>.</returns> | |||
| /// <exception cref="InvalidOperationException">Thrown if the string passed is not valid json.</exception> | |||
| public static EmbedBuilder Parse(string json) | |||
| { | |||
| try | |||
| { | |||
| var model = JsonConvert.DeserializeObject<Embed>(json); | |||
| if (model is not null) | |||
| return model.ToEmbedBuilder(); | |||
| return new EmbedBuilder(); | |||
| } | |||
| catch | |||
| { | |||
| throw; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Sets the title of an <see cref="Embed"/>. | |||
| /// </summary> | |||
| @@ -27,6 +27,12 @@ namespace Discord | |||
| /// </summary> | |||
| public Optional<ulong> GuildId { get; internal set; } | |||
| /// <summary> | |||
| /// Gets whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message | |||
| /// Defaults to true. | |||
| /// </summary> | |||
| public Optional<bool> FailIfNotExists { get; internal set; } | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="MessageReference"/> class. | |||
| /// </summary> | |||
| @@ -39,16 +45,21 @@ namespace Discord | |||
| /// <param name="guildId"> | |||
| /// The ID of the guild that will be referenced. It will be validated if sent. | |||
| /// </param> | |||
| public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null) | |||
| /// <param name="failIfNotExists"> | |||
| /// Whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message. Defaults to true. | |||
| /// </param> | |||
| public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null, bool? failIfNotExists = null) | |||
| { | |||
| MessageId = messageId ?? Optional.Create<ulong>(); | |||
| InternalChannelId = channelId ?? Optional.Create<ulong>(); | |||
| GuildId = guildId ?? Optional.Create<ulong>(); | |||
| FailIfNotExists = failIfNotExists ?? Optional.Create<bool>(); | |||
| } | |||
| 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; | |||
| @@ -7,30 +7,55 @@ namespace Discord | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| public struct ChannelPermissions | |||
| { | |||
| /// <summary> Gets a blank <see cref="ChannelPermissions"/> that grants no permissions.</summary> | |||
| /// <returns> A <see cref="ChannelPermissions"/> structure that does not contain any set permissions.</returns> | |||
| public static readonly ChannelPermissions None = new ChannelPermissions(); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for text channels.</summary> | |||
| public static readonly ChannelPermissions Text = new ChannelPermissions(0b0_11111_0101100_0000000_1111111110001_010001); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels.</summary> | |||
| public static readonly ChannelPermissions Voice = new ChannelPermissions(0b1_00000_0000100_1111110_0000000011100_010001); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels.</summary> | |||
| public static readonly ChannelPermissions Stage = new ChannelPermissions(0b0_00000_1000100_0111010_0000000010000_010001); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels.</summary> | |||
| public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels.</summary> | |||
| public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels.</summary> | |||
| public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); | |||
| /// <summary> Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type.</summary> | |||
| /// <summary> | |||
| /// Gets a blank <see cref="ChannelPermissions"/> that grants no permissions. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A <see cref="ChannelPermissions"/> structure that does not contain any set permissions. | |||
| /// </returns> | |||
| public static readonly ChannelPermissions None = new(); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for text channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions Text = new(0b0_11111_0101100_0000000_1111111110001_010001); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for voice channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions Voice = new(0b1_11111_0101100_1111110_1111111111101_010001); // (0b1_00000_0000100_1111110_0000000011100_010001 (<- voice only perms) |= Text) | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for stage channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions Stage = new(0b0_00000_1000100_0111010_0000000010000_010001); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for category channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions Category = new(0b01100_1111110_1111111110001_010001); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for direct message channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions DM = new(0b00000_1000110_1011100110001_000000); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for group channels. | |||
| /// </summary> | |||
| public static readonly ChannelPermissions Group = new(0b00000_1000110_0001101100000_000000); | |||
| /// <summary> | |||
| /// Gets a <see cref="ChannelPermissions"/> that grants all permissions for a given channel type. | |||
| /// </summary> | |||
| /// <exception cref="ArgumentException">Unknown channel type.</exception> | |||
| public static ChannelPermissions All(IChannel channel) | |||
| { | |||
| return channel switch | |||
| { | |||
| ITextChannel _ => Text, | |||
| IStageChannel _ => Stage, | |||
| IVoiceChannel _ => Voice, | |||
| ITextChannel _ => Text, | |||
| ICategoryChannel _ => Category, | |||
| IDMChannel _ => DM, | |||
| IGroupChannel _ => Group, | |||
| @@ -79,7 +79,7 @@ namespace Discord | |||
| /// Sets a timestamp how long a user should be timed out for. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <see cref="null"/> or a time in the past to clear a currently existing timeout. | |||
| /// <see langword="null"/> or a time in the past to clear a currently existing timeout. | |||
| /// </remarks> | |||
| public Optional<DateTimeOffset?> TimedOutUntil { get; set; } | |||
| } | |||
| @@ -104,7 +104,7 @@ namespace Discord | |||
| /// Gets the date and time that indicates if and for how long a user has been timed out. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// <see cref="null"/> or a timestamp in the past if the user is not timed out. | |||
| /// <see langword="null"/> or a timestamp in the past if the user is not timed out. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A <see cref="DateTimeOffset"/> indicating how long the user will be timed out for. | |||
| @@ -116,7 +116,7 @@ namespace Discord | |||
| /// </summary> | |||
| /// <example> | |||
| /// <para>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 <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference)"/>.</para> | |||
| /// this channel; if so, uploads a file via <see cref="IMessageChannel.SendFileAsync(string, string, bool, Embed, RequestOptions, bool, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[], MessageFlags)"/>.</para> | |||
| /// <code language="cs"> | |||
| /// 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. | |||
| /// </remarks> | |||
| /// <param name="format">The format to return.</param> | |||
| /// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048. | |||
| /// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param> | |||
| /// <returns> | |||
| /// A string representing the URL of the displayed avatar for this user. <see langword="null"/> if the user does not have an avatar in place. | |||
| /// </returns> | |||
| @@ -37,8 +37,9 @@ namespace Discord | |||
| /// <summary> Sanitizes the string, safely escaping any Markdown sequences. </summary> | |||
| 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; | |||
| } | |||
| @@ -23,7 +23,7 @@ namespace Discord.Utils | |||
| /// <summary> | |||
| /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord | |||
| /// <see cref="Validate(string)"/> should be used everything other than url buttons. | |||
| /// <see cref="Validate(string, bool)"/> should be used everything other than url buttons. | |||
| /// </summary> | |||
| /// <param name="url">The URL to validate before sending to discord.</param> | |||
| /// <exception cref="InvalidOperationException">A URL must include a protocol (either http, https, or discord).</exception> | |||
| @@ -1,7 +1,7 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <TargetFramework>net5.0</TargetFramework> | |||
| <TargetFramework>net6.0</TargetFramework> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| @@ -3,7 +3,7 @@ using System; | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/>. | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/>. | |||
| /// </summary> | |||
| [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||
| public class AutocompleteAttribute : Attribute | |||
| @@ -14,7 +14,7 @@ namespace Discord.Interactions | |||
| public Type AutocompleteHandlerType { get; } | |||
| /// <summary> | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> and define a <see cref="AutocompleteHandler"/> to handle | |||
| /// Autocomplete interactions targeting the parameter this <see cref="Attribute"/> is applied to. | |||
| /// </summary> | |||
| /// <remarks> | |||
| @@ -29,7 +29,7 @@ namespace Discord.Interactions | |||
| } | |||
| /// <summary> | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.Autocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>. | |||
| /// Set the <see cref="ApplicationCommandOptionProperties.IsAutocomplete"/> to <see langword="true"/> without specifying a <see cref="AutocompleteHandler"/>. | |||
| /// </summary> | |||
| public AutocompleteAttribute() { } | |||
| } | |||
| @@ -21,9 +21,7 @@ namespace Discord.Interactions | |||
| /// <summary> | |||
| /// Create a new <see cref="ModalInputAttribute"/>. | |||
| /// </summary> | |||
| /// <param name="label">The label of the input.</param> | |||
| /// <param name="customId">The custom id of the input.</param> | |||
| /// <param name="required">Whether the user is required to input a value.></param> | |||
| protected ModalInputAttribute(string customId) | |||
| { | |||
| CustomId = customId; | |||
| @@ -36,7 +36,7 @@ namespace Discord.Interactions | |||
| /// <summary> | |||
| /// Create a new <see cref="ModalTextInputAttribute"/>. | |||
| /// </summary> | |||
| /// <param name="customId"The custom id of the text input.></param> | |||
| /// <param name="customId">The custom id of the text input.></param> | |||
| /// <param name="style">The style of the text input.</param> | |||
| /// <param name="placeholder">The placeholder of the text input.</param> | |||
| /// <param name="minLength">The minimum length of the text input's content.</param> | |||
| @@ -29,7 +29,7 @@ namespace Discord.Interactions | |||
| /// <remarks> | |||
| /// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>. | |||
| /// </remarks> | |||
| /// <param name="permission"> | |||
| /// <param name="guildPermission"> | |||
| /// The <see cref="Discord.GuildPermission" /> that the user must have. Multiple permissions can be | |||
| /// specified by ORing the permissions together. | |||
| /// </param> | |||
| @@ -41,7 +41,7 @@ namespace Discord.Interactions | |||
| /// <summary> | |||
| /// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>. | |||
| /// </summary> | |||
| /// <param name="permission"> | |||
| /// <param name="channelPermission"> | |||
| /// The <see cref="Discord.ChannelPermission"/> that the user must have. Multiple permissions can be | |||
| /// specified by ORing the permissions together. | |||
| /// </param> | |||
| @@ -56,7 +56,7 @@ namespace Discord.Interactions.Builders | |||
| /// <summary> | |||
| /// Sets <see cref="DefaultPermission"/>. | |||
| /// </summary> | |||
| /// <param name="defaultPermision">New value of the <see cref="DefaultPermission"/>.</param> | |||
| /// <param name="permission">New value of the <see cref="DefaultPermission"/>.</param> | |||
| /// <returns> | |||
| /// The builder instance. | |||
| /// </returns> | |||
| @@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders | |||
| /// <summary> | |||
| /// Sets <see cref="Style"/>. | |||
| /// </summary> | |||
| /// <param name="style">New value of the <see cref="SetValue(string)"/>.</param> | |||
| /// <param name="style">New value of the <see cref="Style"/>.</param> | |||
| /// <returns> | |||
| /// The builder instance. | |||
| /// </returns> | |||
| @@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders | |||
| } | |||
| /// <summary> | |||
| /// Adds text components to <see cref="TextComponents"/>. | |||
| /// Adds text components to <see cref="Components"/>. | |||
| /// </summary> | |||
| /// <param name="configure">Text Component builder factory.</param> | |||
| /// <returns> | |||
| @@ -357,7 +357,8 @@ namespace Discord.Interactions.Builders | |||
| return this; | |||
| } | |||
| /// <summary> | |||
| /// Adds a modal command builder to <see cref="ModalCommands"/>. | |||
| /// </summary> | |||
| /// <param name="configure"><see cref="ModalCommands"/> factory.</param> | |||
| @@ -122,7 +122,7 @@ namespace Discord.Interactions.Builders | |||
| /// <summary> | |||
| /// Adds preconditions to <see cref="Preconditions"/> | |||
| /// </summary> | |||
| /// <param name="preconditions">New attributes to be added to <see cref="Preconditions"/>.</param> | |||
| /// <param name="attributes">New attributes to be added to <see cref="Preconditions"/>.</param> | |||
| /// <returns> | |||
| /// The builder instance. | |||
| /// </returns> | |||
| @@ -7,8 +7,10 @@ | |||
| <RootNamespace>Discord.Interactions</RootNamespace> | |||
| <AssemblyName>Discord.Net.Interactions</AssemblyName> | |||
| <Description>A Discord.Net extension adding support for Application Commands.</Description> | |||
| <WarningLevel>5</WarningLevel> | |||
| <TreatWarningsAsErrors>True</TreatWarningsAsErrors> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||
| @@ -248,7 +248,7 @@ namespace Discord.Interactions | |||
| while (parent != null) | |||
| { | |||
| permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0); | |||
| permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0).SanitizeGuildPermissions(); | |||
| parent = parent.Parent; | |||
| } | |||
| @@ -24,8 +24,7 @@ namespace Discord.Interactions | |||
| /// </summary> | |||
| /// <param name="client">The underlying client.</param> | |||
| /// <param name="interaction">The underlying interaction.</param> | |||
| /// <param name="user"><see cref="IUser"/> who executed the command.</param> | |||
| /// <param name="channel"><see cref="ISocketMessageChannel"/> the command originated from.</param> | |||
| /// <param name="channel"><see cref="IMessageChannel"/> the command originated from.</param> | |||
| public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IMessageChannel channel = null) | |||
| { | |||
| Client = client; | |||
| @@ -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); | |||
| /// <inheritdoc cref="IDiscordInteraction.RespondAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/> | |||
| /// <inheritdoc cref="IDiscordInteraction.RespondAsync(string, Embed[], bool, bool, AllowedMentions, MessageComponent, Embed, RequestOptions)"/> | |||
| 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); | |||
| /// <inheritdoc cref="IDiscordInteraction.FollowupAsync(string, Embed[], bool, bool, AllowedMentions, RequestOptions, MessageComponent, Embed)"/> | |||
| /// <inheritdoc cref="IDiscordInteraction.FollowupAsync(string, Embed[], bool, bool, AllowedMentions, MessageComponent, Embed, RequestOptions)"/> | |||
| protected virtual async Task<IUserMessage> 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); | |||
| /// <inheritdoc cref="IMessageChannel.SendMessageAsync(string, bool, Embed, RequestOptions, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[])"/> | |||
| /// <inheritdoc cref="IMessageChannel.SendMessageAsync(string, bool, Embed, RequestOptions, AllowedMentions, MessageReference, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| protected virtual async Task<IUserMessage> 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 | |||
| /// <inheritdoc cref="IDiscordInteraction.RespondWithModalAsync(Modal, RequestOptions)"/> | |||
| protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal); | |||
| /// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync(IDiscordInteraction, IModal, RequestOptions)"/> | |||
| protected virtual async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null) where T : class, IModal | |||
| => await Context.Interaction.RespondWithModalAsync<T>(customId, options); | |||
| /// <inheritdoc cref="IDiscordInteractionExtentions.RespondWithModalAsync{T}(IDiscordInteraction, string, RequestOptions, Action{ModalBuilder})"/> | |||
| protected virtual async Task RespondWithModalAsync<TModal>(string customId, RequestOptions options = null) where TModal : class, IModal | |||
| => await Context.Interaction.RespondWithModalAsync<TModal>(customId, options); | |||
| //IInteractionModuleBase | |||
| @@ -223,7 +223,8 @@ namespace Discord.Interactions | |||
| new ConcurrentDictionary<Type, Type> | |||
| { | |||
| [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), | |||
| [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) | |||
| [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), | |||
| [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) | |||
| }); | |||
| _typeReaderMap = new TypeMap<TypeReader, string>(this, new ConcurrentDictionary<Type, TypeReader>(), | |||
| @@ -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,20 +423,39 @@ namespace Discord.Interactions | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect, | |||
| /// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail. | |||
| /// use <see cref="AddModulesToGuildAsync(IGuild, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail. | |||
| /// </remarks> | |||
| /// <param name="guild">The target guild.</param> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="commands">Commands to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| /// </returns> | |||
| public async Task<IReadOnlyCollection<RestGuildCommand>> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) | |||
| { | |||
| EnsureClientReady(); | |||
| if (guild is null) | |||
| throw new ArgumentNullException(nameof(guild)); | |||
| return await AddCommandsToGuildAsync(guild.Id, deleteMissing, commands).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Register Application Commands from <paramref name="commands"/> to a guild. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect, | |||
| /// use <see cref="AddModulesToGuildAsync(ulong, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail. | |||
| /// </remarks> | |||
| /// <param name="guildId">The target guild ID.</param> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="commands">Commands to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| /// </returns> | |||
| public async Task<IReadOnlyCollection<RestGuildCommand>> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) | |||
| { | |||
| EnsureClientReady(); | |||
| var props = new List<ApplicationCommandProperties>(); | |||
| foreach (var command in commands) | |||
| @@ -454,44 +475,60 @@ namespace Discord.Interactions | |||
| if (!deleteMissing) | |||
| { | |||
| var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); | |||
| var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); | |||
| var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); | |||
| props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); | |||
| } | |||
| return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); | |||
| return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Register Application Commands from modules provided in <paramref name="modules"/> to a guild. | |||
| /// </summary> | |||
| /// <param name="guild">The target guild.</param> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="modules">Modules to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| /// </returns> | |||
| public async Task<IReadOnlyCollection<RestGuildCommand>> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) | |||
| { | |||
| EnsureClientReady(); | |||
| if (guild is null) | |||
| throw new ArgumentNullException(nameof(guild)); | |||
| return await AddModulesToGuildAsync(guild.Id, deleteMissing, modules).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Register Application Commands from modules provided in <paramref name="modules"/> to a guild. | |||
| /// </summary> | |||
| /// <param name="guildId">The target guild ID.</param> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="modules">Modules to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| /// </returns> | |||
| public async Task<IReadOnlyCollection<RestGuildCommand>> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) | |||
| { | |||
| EnsureClientReady(); | |||
| var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); | |||
| if (!deleteMissing) | |||
| { | |||
| var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); | |||
| var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); | |||
| var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); | |||
| props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); | |||
| } | |||
| return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); | |||
| return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Register Application Commands from modules provided in <paramref name="modules"/> as global commands. | |||
| /// </summary> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="modules">Modules to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| @@ -517,8 +554,9 @@ namespace Discord.Interactions | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Commands will be registered as standalone commands, if you want the <see cref="GroupAttribute"/> to take effect, | |||
| /// use <see cref="AddModulesToGuildAsync(IGuild, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail. | |||
| /// use <see cref="AddModulesToGuildAsync(IGuild, bool, ModuleInfo[])"/>. Registering a commands without group names might cause the command traversal to fail. | |||
| /// </remarks> | |||
| /// <param name="deleteMissing">If <see langword="false"/>, this operation will not delete the commands that are missing from <see cref="InteractionService"/>.</param> | |||
| /// <param name="commands">Commands to be registered to Discord.</param> | |||
| /// <returns> | |||
| /// A task representing the command registration process. The task result contains the active application commands of the target guild. | |||
| @@ -834,11 +872,16 @@ namespace Discord.Interactions | |||
| if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) | |||
| return; | |||
| var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; | |||
| for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) | |||
| matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); | |||
| 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); | |||
| matchContainer.SetSegmentMatches(matches); | |||
| } | |||
| else | |||
| matchContainer.SetSegmentMatches(Array.Empty<RouteSegmentMatch>()); | |||
| } | |||
| internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) | |||
| @@ -960,7 +1003,7 @@ namespace Discord.Interactions | |||
| /// Removes a type reader for the given type. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// You need to reload the modules for the changes to take effect. | |||
| /// </remarks> | |||
| /// <param name="type">The type to remove the reader from.</param> | |||
| @@ -973,7 +1016,7 @@ namespace Discord.Interactions | |||
| /// Removes a generic type reader from the type <typeparamref name="T"/>. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// You need to reload the modules for the changes to take effect. | |||
| /// </remarks> | |||
| /// <typeparam name="T">The type to remove the readers from.</typeparam> | |||
| @@ -986,7 +1029,7 @@ namespace Discord.Interactions | |||
| /// Removes a generic type reader from the given type. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// You need to reload the modules for the changes to take effect. | |||
| /// </remarks> | |||
| /// <param name="type">The type to remove the reader from.</param> | |||
| @@ -999,7 +1042,7 @@ namespace Discord.Interactions | |||
| /// Serialize an object using a <see cref="TypeReader"/> into a <see cref="string"/> to be placed in a Component CustomId. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="CommandService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// Removing a <see cref="TypeReader"/> from the <see cref="InteractionService"/> will not dereference the <see cref="TypeReader"/> from the loaded module/command instances. | |||
| /// You need to reload the modules for the changes to take effect. | |||
| /// </remarks> | |||
| /// <typeparam name="T">Type of the object to be serialized.</typeparam> | |||
| @@ -1079,19 +1122,40 @@ namespace Discord.Interactions | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild, | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, | |||
| params ApplicationCommandPermission[] permissions) | |||
| { | |||
| if (module is null) | |||
| throw new ArgumentNullException(nameof(module)); | |||
| if (guild is null) | |||
| throw new ArgumentNullException(nameof(guild)); | |||
| return await ModifySlashCommandPermissionsAsync(module, guild.Id, permissions).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Modify the command permissions of the matching Discord Slash Command. | |||
| /// </summary> | |||
| /// <param name="module">Module representing the top level Slash Command.</param> | |||
| /// <param name="guildId">Target guild ID.</param> | |||
| /// <param name="permissions">New permission values.</param> | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, | |||
| params ApplicationCommandPermission[] permissions) | |||
| { | |||
| if (module is null) | |||
| throw new ArgumentNullException(nameof(module)); | |||
| if (!module.IsSlashGroup) | |||
| throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); | |||
| if (!module.IsTopLevelGroup) | |||
| throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); | |||
| if (guild is null) | |||
| throw new ArgumentNullException("guild"); | |||
| var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); | |||
| var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); | |||
| var appCommand = commands.First(x => x.Name == module.SlashGroupName); | |||
| return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); | |||
| @@ -1106,9 +1170,29 @@ namespace Discord.Interactions | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild, | |||
| params ApplicationCommandPermission[] permissions) => | |||
| await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, | |||
| params ApplicationCommandPermission[] permissions) | |||
| { | |||
| if (command is null) | |||
| throw new ArgumentNullException(nameof(command)); | |||
| if (guild is null) | |||
| throw new ArgumentNullException(nameof(guild)); | |||
| return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false); | |||
| } | |||
| /// <summary> | |||
| /// Modify the command permissions of the matching Discord Slash Command. | |||
| /// </summary> | |||
| /// <param name="command">The Slash Command.</param> | |||
| /// <param name="guildId">Target guild ID.</param> | |||
| /// <param name="permissions">New permission values.</param> | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, | |||
| params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); | |||
| /// <summary> | |||
| /// Modify the command permissions of the matching Discord Slash Command. | |||
| @@ -1119,20 +1203,40 @@ namespace Discord.Interactions | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild, | |||
| params ApplicationCommandPermission[] permissions) => | |||
| await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); | |||
| public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, | |||
| params ApplicationCommandPermission[] permissions) | |||
| { | |||
| if (command is null) | |||
| throw new ArgumentNullException(nameof(command)); | |||
| if (guild is null) | |||
| throw new ArgumentNullException(nameof(guild)); | |||
| return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false); | |||
| } | |||
| private async Task<GuildApplicationCommandPermission> ModifyApplicationCommandPermissionsAsync<T> (T command, IGuild guild, | |||
| /// <summary> | |||
| /// Modify the command permissions of the matching Discord Slash Command. | |||
| /// </summary> | |||
| /// <param name="command">The Context Command.</param> | |||
| /// <param name="guildId">Target guild ID.</param> | |||
| /// <param name="permissions">New permission values.</param> | |||
| /// <returns> | |||
| /// The active command permissions after the modification. | |||
| /// </returns> | |||
| public async Task<GuildApplicationCommandPermission> ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, | |||
| params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); | |||
| private async Task<GuildApplicationCommandPermission> ModifyApplicationCommandPermissionsAsync<T> (T command, ulong guildId, | |||
| params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo | |||
| { | |||
| if (command is null) | |||
| throw new ArgumentNullException(nameof(command)); | |||
| if (!command.IsTopLevelCommand) | |||
| throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); | |||
| if (guild is null) | |||
| throw new ArgumentNullException("guild"); | |||
| var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); | |||
| var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); | |||
| var appCommand = commands.First(x => x.Name == ( command as IApplicationCommandInfo ).Name); | |||
| return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); | |||
| @@ -87,12 +87,12 @@ namespace Discord.Interactions | |||
| await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); | |||
| } | |||
| protected override async Task RespondWithModalAsync<T>(string customId, RequestOptions options = null) | |||
| protected override async Task RespondWithModalAsync<TModal>(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<T>(customId, options); | |||
| var payload = restInteraction.RespondWithModal<TModal>(customId, options); | |||
| if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) | |||
| await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); | |||
| @@ -3,7 +3,7 @@ using System; | |||
| namespace Discord.Interactions | |||
| { | |||
| /// <summary> | |||
| /// Represents a result type for <see cref="TypeConverter.ReadAsync(IInteractionContext, WebSocket.SocketSlashCommandDataOption, IServiceProvider)"/>. | |||
| /// Represents a result type for <see cref="TypeConverter.ReadAsync(IInteractionContext, IApplicationCommandInteractionDataOption, IServiceProvider)"/>. | |||
| /// </summary> | |||
| public struct TypeConverterResult : IResult | |||
| { | |||
| @@ -0,0 +1,23 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Interactions | |||
| { | |||
| internal class NullableComponentConverter<T> : ComponentTypeConverter<T> | |||
| { | |||
| 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<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) | |||
| => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Interactions | |||
| { | |||
| internal class NullableReader<T> : TypeReader<T> | |||
| { | |||
| 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<TypeConverterResult> ReadAsync(IInteractionContext context, string option, IServiceProvider services) | |||
| => string.IsNullOrEmpty(option) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeReader.ReadAsync(context, option, services); | |||
| } | |||
| } | |||
| @@ -41,7 +41,7 @@ namespace Discord.Interactions | |||
| Name = commandInfo.Name, | |||
| Description = commandInfo.Description, | |||
| IsDMEnabled = commandInfo.IsEnabledInDm, | |||
| DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0) | |||
| DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | |||
| }.Build(); | |||
| if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) | |||
| @@ -69,14 +69,14 @@ namespace Discord.Interactions | |||
| { | |||
| Name = commandInfo.Name, | |||
| IsDefaultPermission = commandInfo.DefaultPermission, | |||
| DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0), | |||
| 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), | |||
| DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), | |||
| IsDMEnabled = commandInfo.IsEnabledInDm | |||
| }.Build(), | |||
| _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") | |||
| @@ -232,5 +232,8 @@ namespace Discord.Interactions | |||
| return builder.Build(); | |||
| } | |||
| public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) => | |||
| permissions == 0 ? null : permissions; | |||
| } | |||
| } | |||
| @@ -66,5 +66,12 @@ namespace Discord.API | |||
| [JsonProperty("member_count")] | |||
| public Optional<int> MemberCount { get; set; } | |||
| //ForumChannel | |||
| [JsonProperty("available_tags")] | |||
| public Optional<ForumTags[]> ForumTags { get; set; } | |||
| [JsonProperty("default_auto_archive_duration")] | |||
| public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; } | |||
| } | |||
| } | |||
| @@ -9,8 +9,5 @@ namespace Discord.API.Rest | |||
| [JsonProperty("members")] | |||
| public ThreadMember[] Members { get; set; } | |||
| [JsonProperty("has_more")] | |||
| public bool HasMore { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API | |||
| { | |||
| internal class ForumTags | |||
| { | |||
| [JsonProperty("id")] | |||
| public ulong Id { get; set; } | |||
| [JsonProperty("name")] | |||
| public string Name { get; set; } | |||
| [JsonProperty("emoji_id")] | |||
| public Optional<ulong?> EmojiId { get; set; } | |||
| [JsonProperty("emoji_name")] | |||
| public Optional<string> EmojiName { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API | |||
| { | |||
| internal class ForumThreadMessage | |||
| { | |||
| [JsonProperty("content")] | |||
| public Optional<string> Content { get; set; } | |||
| [JsonProperty("nonce")] | |||
| public Optional<string> Nonce { get; set; } | |||
| [JsonProperty("embeds")] | |||
| public Optional<Embed[]> Embeds { get; set; } | |||
| [JsonProperty("allowed_mentions")] | |||
| public Optional<AllowedMentions> AllowedMentions { get; set; } | |||
| [JsonProperty("components")] | |||
| public Optional<API.ActionRowComponent[]> Components { get; set; } | |||
| [JsonProperty("sticker_ids")] | |||
| public Optional<ulong[]> Stickers { get; set; } | |||
| [JsonProperty("flags")] | |||
| public Optional<MessageFlags> Flags { get; set; } | |||
| } | |||
| } | |||
| @@ -12,5 +12,8 @@ namespace Discord.API | |||
| [JsonProperty("guild_id")] | |||
| public Optional<ulong> GuildId { get; set; } | |||
| [JsonProperty("fail_if_not_exists")] | |||
| public Optional<bool> FailIfNotExists { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,96 @@ | |||
| using Discord.Net.Converters; | |||
| using Discord.Net.Rest; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API.Rest | |||
| { | |||
| internal class CreateMultipartPostAsync | |||
| { | |||
| private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| public FileAttachment[] Files { get; } | |||
| public string Title { get; set; } | |||
| public ThreadArchiveDuration ArchiveDuration { get; set; } | |||
| public Optional<int?> Slowmode { get; set; } | |||
| public Optional<string> Content { get; set; } | |||
| public Optional<Embed[]> Embeds { get; set; } | |||
| public Optional<AllowedMentions> AllowedMentions { get; set; } | |||
| public Optional<ActionRowComponent[]> MessageComponent { get; set; } | |||
| public Optional<MessageFlags?> Flags { get; set; } | |||
| public Optional<ulong[]> Stickers { get; set; } | |||
| public CreateMultipartPostAsync(params FileAttachment[] attachments) | |||
| { | |||
| Files = attachments; | |||
| } | |||
| public IReadOnlyDictionary<string, object> ToDictionary() | |||
| { | |||
| var d = new Dictionary<string, object>(); | |||
| var payload = new Dictionary<string, object>(); | |||
| var message = new Dictionary<string, object>(); | |||
| payload["name"] = Title; | |||
| payload["auto_archive_duration"] = ArchiveDuration; | |||
| if (Slowmode.IsSpecified) | |||
| payload["rate_limit_per_user"] = Slowmode.Value; | |||
| // message | |||
| if (Content.IsSpecified) | |||
| message["content"] = Content.Value; | |||
| if (Embeds.IsSpecified) | |||
| message["embeds"] = Embeds.Value; | |||
| if (AllowedMentions.IsSpecified) | |||
| message["allowed_mentions"] = AllowedMentions.Value; | |||
| if (MessageComponent.IsSpecified) | |||
| message["components"] = MessageComponent.Value; | |||
| if (Stickers.IsSpecified) | |||
| message["sticker_ids"] = Stickers.Value; | |||
| if (Flags.IsSpecified) | |||
| message["flags"] = Flags.Value; | |||
| List<object> attachments = new(); | |||
| for (int n = 0; n != Files.Length; n++) | |||
| { | |||
| var attachment = Files[n]; | |||
| var filename = attachment.FileName ?? "unknown.dat"; | |||
| if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) | |||
| filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); | |||
| d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); | |||
| attachments.Add(new | |||
| { | |||
| id = (ulong)n, | |||
| filename = filename, | |||
| description = attachment.Description ?? Optional<string>.Unspecified | |||
| }); | |||
| } | |||
| message["attachments"] = attachments; | |||
| payload["message"] = message; | |||
| var json = new StringBuilder(); | |||
| using (var text = new StringWriter(json)) | |||
| using (var writer = new JsonTextWriter(text)) | |||
| _serializer.Serialize(writer, payload); | |||
| d["payload_json"] = json.ToString(); | |||
| return d; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.API.Rest | |||
| { | |||
| internal class CreatePostParams | |||
| { | |||
| // thread | |||
| [JsonProperty("name")] | |||
| public string Title { get; set; } | |||
| [JsonProperty("auto_archive_duration")] | |||
| public ThreadArchiveDuration ArchiveDuration { get; set; } | |||
| [JsonProperty("rate_limit_per_user")] | |||
| public Optional<int?> Slowmode { get; set; } | |||
| [JsonProperty("message")] | |||
| public ForumThreadMessage Message { get; set; } | |||
| } | |||
| } | |||
| @@ -37,7 +37,7 @@ namespace Discord.API.Rest | |||
| if (Content.IsSpecified) | |||
| payload["content"] = Content.Value; | |||
| if (IsTTS.IsSpecified) | |||
| payload["tts"] = IsTTS.Value.ToString(); | |||
| payload["tts"] = IsTTS.Value; | |||
| if (Nonce.IsSpecified) | |||
| payload["nonce"] = Nonce.Value; | |||
| if (Embeds.IsSpecified) | |||
| @@ -50,7 +50,7 @@ namespace Discord.API.Rest | |||
| if (Content.IsSpecified) | |||
| data["content"] = Content.Value; | |||
| if (IsTTS.IsSpecified) | |||
| data["tts"] = IsTTS.Value.ToString(); | |||
| data["tts"] = IsTTS.Value; | |||
| if (MessageComponents.IsSpecified) | |||
| data["components"] = MessageComponents.Value; | |||
| if (Embeds.IsSpecified) | |||
| @@ -36,7 +36,7 @@ namespace Discord.API.Rest | |||
| if (Content.IsSpecified) | |||
| payload["content"] = Content.Value; | |||
| if (IsTTS.IsSpecified) | |||
| payload["tts"] = IsTTS.Value.ToString(); | |||
| payload["tts"] = IsTTS.Value; | |||
| if (Nonce.IsSpecified) | |||
| payload["nonce"] = Nonce.Value; | |||
| if (Username.IsSpecified) | |||
| @@ -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))] | |||
| @@ -7,6 +7,8 @@ | |||
| <Description>A core Discord.Net library containing the REST client and models.</Description> | |||
| <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net6.0;net5.0;net461;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net6.0;net5.0;netstandard2.0;netstandard2.1</TargetFrameworks> | |||
| <WarningLevel>5</WarningLevel> | |||
| <TreatWarningsAsErrors>True</TreatWarningsAsErrors> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| @@ -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)); | |||
| @@ -464,6 +466,24 @@ namespace Discord.API | |||
| #endregion | |||
| #region Threads | |||
| public async Task<Channel> CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||
| var bucket = new BucketIds(channelId: channelId); | |||
| return await SendJsonAsync<Channel>("POST", () => $"channels/{channelId}/threads", args, bucket, options: options); | |||
| } | |||
| public async Task<Channel> CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||
| var bucket = new BucketIds(channelId: channelId); | |||
| return await SendMultipartAsync<Channel>("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options); | |||
| } | |||
| public async Task<Channel> ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||
| @@ -564,15 +584,15 @@ namespace Discord.API | |||
| return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | |||
| } | |||
| public async Task<ChannelThreads> GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) | |||
| public async Task<ChannelThreads> GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) | |||
| { | |||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var bucket = new BucketIds(channelId: channelId); | |||
| var bucket = new BucketIds(guildId: guildId); | |||
| return await SendAsync<ChannelThreads>("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); | |||
| return await SendAsync<ChannelThreads>("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options); | |||
| } | |||
| public async Task<ChannelThreads> GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) | |||
| @@ -671,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) { } | |||
| } | |||
| @@ -798,9 +820,11 @@ namespace Discord.API | |||
| var ids = new BucketIds(channelId: channelId); | |||
| return await SendJsonAsync<Message>("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| } | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception> | |||
| public async Task<Message> CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) | |||
| public async Task<Message> 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."); | |||
| @@ -816,12 +840,12 @@ namespace Discord.API | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var ids = new BucketIds(webhookId: webhookId); | |||
| return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| } | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception> | |||
| 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."); | |||
| @@ -837,11 +861,11 @@ namespace Discord.API | |||
| options = RequestOptions.CreateOrClone(options); | |||
| var ids = new BucketIds(webhookId: webhookId); | |||
| await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| await SendJsonAsync<Message>("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| } | |||
| /// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception> | |||
| 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."); | |||
| @@ -852,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); | |||
| } | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| @@ -873,7 +897,7 @@ namespace Discord.API | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="InvalidOperationException">This operation may only be called with a <see cref="TokenType.Webhook"/> token.</exception> | |||
| public async Task<Message> UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) | |||
| public async Task<Message> 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."); | |||
| @@ -893,7 +917,7 @@ namespace Discord.API | |||
| } | |||
| var ids = new BucketIds(webhookId: webhookId); | |||
| return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| return await SendMultipartAsync<Message>("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) | |||
| { | |||
| @@ -1380,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); | |||
| @@ -1400,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<Message>("POST", () => $"webhooks/{CurrentApplicationId}/{token}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); | |||
| } | |||
| @@ -1729,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); | |||
| @@ -2414,6 +2440,18 @@ namespace Discord.API | |||
| return (expr as MemberExpression).Member.Name; | |||
| } | |||
| private static string WebhookQuery(bool wait = false, ulong? threadId = null) | |||
| { | |||
| List<string> querys = new List<string>() { }; | |||
| if (wait) | |||
| querys.Add("wait=true"); | |||
| if (threadId.HasValue) | |||
| querys.Add($"thread_id={threadId}"); | |||
| return $"{string.Join("&", querys)}"; | |||
| } | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -32,9 +32,15 @@ namespace Discord.Rest | |||
| /// Initializes a new <see cref="DiscordRestClient"/> with the provided configuration. | |||
| /// </summary> | |||
| /// <param name="config">The configuration to be used with the client.</param> | |||
| 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 <see cref="RestInteraction"/> that represents the incoming http interaction. | |||
| /// </returns> | |||
| /// <exception cref="BadSignatureException">Thrown when the signature doesn't match the public key.</exception> | |||
| public Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body) | |||
| => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); | |||
| public Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func<InteractionProperties, bool> doApiCallOnCreation = null) | |||
| => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation); | |||
| /// <summary> | |||
| /// Creates a <see cref="RestInteraction"/> from a http message. | |||
| @@ -127,7 +135,7 @@ namespace Discord.Rest | |||
| /// A <see cref="RestInteraction"/> that represents the incoming http interaction. | |||
| /// </returns> | |||
| /// <exception cref="BadSignatureException">Thrown when the signature doesn't match the public key.</exception> | |||
| public async Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body) | |||
| public async Task<RestInteraction> ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func<InteractionProperties, bool> 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<API.Interaction>(jsonReader); | |||
| return await RestInteraction.CreateAsync(this, model); | |||
| return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation is not null ? doApiCallOnCreation(new InteractionProperties(model)) : _apiOnCreation); | |||
| } | |||
| } | |||
| #endregion | |||
| public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null) | |||
| { | |||
| return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false); | |||
| @@ -9,5 +9,7 @@ namespace Discord.Rest | |||
| { | |||
| /// <summary> Gets or sets the provider used to generate new REST connections. </summary> | |||
| public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; | |||
| public bool APIOnRestInteractionCreation { get; set; } = true; | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| /// Gets the user that was banned. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the banned user. | |||
| /// </returns> | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| /// Gets the bot that was added. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the bot is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the bot. | |||
| /// </returns> | |||
| @@ -45,7 +45,7 @@ namespace Discord.Rest | |||
| { | |||
| var inviterId = inviterIdModel.NewValue.ToObject<ulong>(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 | |||
| /// <summary> | |||
| /// Gets the user that created this invite if available. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user that created this invite or <see langword="null"/>. | |||
| /// </returns> | |||
| @@ -45,7 +45,7 @@ namespace Discord.Rest | |||
| { | |||
| var inviterId = inviterIdModel.OldValue.ToObject<ulong>(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 | |||
| /// <summary> | |||
| /// Gets the user that created this invite if available. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user that created this invite or <see langword="null"/>. | |||
| /// </returns> | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| /// Gets the user that was kicked. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the kicked user. | |||
| /// </returns> | |||
| @@ -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); | |||
| } | |||
| @@ -33,7 +33,7 @@ namespace Discord.Rest | |||
| newMute = muteModel?.NewValue?.ToObject<bool>(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 | |||
| /// <summary> | |||
| /// Gets the user that the changes were performed on. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the user who the changes were performed on. | |||
| /// </returns> | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| @@ -41,6 +42,9 @@ namespace Discord.Rest | |||
| /// <summary> | |||
| /// Gets the user of the messages that were deleted. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the user that created the deleted messages. | |||
| /// </returns> | |||
| @@ -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 | |||
| /// <summary> | |||
| /// Gets the user of the message that was pinned if available. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the user that created the pinned message or <see langword="null"/>. | |||
| /// </returns> | |||
| @@ -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 | |||
| /// <summary> | |||
| /// Gets the user of the message that was unpinned if available. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// Will be <see langword="null"/> if the user is a 'Deleted User#....' because Discord does send user data for deleted users. | |||
| /// </remarks> | |||
| /// <returns> | |||
| /// A user object representing the user that created the unpinned message or <see langword="null"/>. | |||
| /// </returns> | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| @@ -0,0 +1,131 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Channel; | |||
| namespace Discord.Rest | |||
| { | |||
| /// <summary> | |||
| /// Represents a REST-based forum channel in a guild. | |||
| /// </summary> | |||
| public class RestForumChannel : RestGuildChannel, IForumChannel | |||
| { | |||
| /// <inheritdoc/> | |||
| public bool IsNsfw { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Topic { get; private set; } | |||
| /// <inheritdoc/> | |||
| public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<ForumTag> Tags { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string Mention => MentionUtils.MentionChannel(Id); | |||
| internal RestForumChannel(BaseDiscordClient client, IGuild guild, ulong id) | |||
| : base(client, guild, id) | |||
| { | |||
| } | |||
| internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) | |||
| { | |||
| var entity = new RestStageChannel(discord, guild, model.Id); | |||
| entity.Update(model); | |||
| return entity; | |||
| } | |||
| internal override void Update(Model model) | |||
| { | |||
| base.Update(model); | |||
| IsNsfw = model.Nsfw.GetValueOrDefault(false); | |||
| Topic = model.Topic.GetValueOrDefault(); | |||
| DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); | |||
| Tags = model.ForumTags.GetValueOrDefault(Array.Empty<API.ForumTags>()).Select( | |||
| x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) | |||
| ).ToImmutableArray(); | |||
| } | |||
| /// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); | |||
| /// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | |||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | |||
| ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| { | |||
| using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); | |||
| return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, | |||
| AllowedMentions allowedMentions = null, MessageComponent components = null, | |||
| ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| { | |||
| using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); | |||
| return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| public Task<RestThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | |||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); | |||
| /// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/> | |||
| public Task<RestThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, | |||
| int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, | |||
| MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); | |||
| /// <inheritdoc cref="IForumChannel.GetActiveThreadsAsync(RequestOptions)"/> | |||
| public Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null) | |||
| => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); | |||
| /// <inheritdoc cref="IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||
| public Task<IReadOnlyCollection<RestThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||
| => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); | |||
| /// <inheritdoc cref="IForumChannel.GetPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||
| public Task<IReadOnlyCollection<RestThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||
| => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); | |||
| /// <inheritdoc cref="IForumChannel.GetPublicArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||
| public Task<IReadOnlyCollection<RestThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||
| => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); | |||
| #region IForumChannel | |||
| async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetActiveThreadsAsync(RequestOptions options) | |||
| => await GetActiveThreadsAsync(options).ConfigureAwait(false); | |||
| async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||
| => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||
| async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||
| => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||
| async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||
| => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||
| async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); | |||
| async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -39,6 +39,7 @@ namespace Discord.Rest | |||
| ChannelType.Text => RestTextChannel.Create(discord, guild, model), | |||
| ChannelType.Voice => RestVoiceChannel.Create(discord, guild, model), | |||
| ChannelType.Stage => RestStageChannel.Create(discord, guild, model), | |||
| ChannelType.Forum => RestForumChannel.Create(discord, guild, model), | |||
| ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), | |||
| ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread => RestThreadChannel.Create(discord, guild, model), | |||
| _ => new RestGuildChannel(discord, guild, model.Id), | |||
| @@ -12,7 +12,11 @@ namespace Discord.Rest | |||
| public class RestStageChannel : RestVoiceChannel, IStageChannel | |||
| { | |||
| /// <inheritdoc/> | |||
| public string Topic { get; private set; } | |||
| /// <remarks> | |||
| /// This field is always false for stage channels. | |||
| /// </remarks> | |||
| public override bool IsTextInVoice | |||
| => false; | |||
| /// <inheritdoc/> | |||
| 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; | |||
| } | |||
| @@ -21,11 +21,12 @@ namespace Discord.Rest | |||
| public virtual int SlowModeInterval { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? CategoryId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Mention => MentionUtils.MentionChannel(Id); | |||
| /// <inheritdoc /> | |||
| public bool IsNsfw { get; private set; } | |||
| /// <inheritdoc /> | |||
| 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 | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -86,25 +93,25 @@ namespace Discord.Rest | |||
| => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); | |||
| /// <inheritdoc /> | |||
| public Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | |||
| public virtual Task<RestMessage> GetMessageAsync(ulong id, RequestOptions options = null) | |||
| => ChannelHelper.GetMessageAsync(this, Discord, id, options); | |||
| /// <inheritdoc /> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); | |||
| /// <inheritdoc /> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); | |||
| /// <inheritdoc /> | |||
| public IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| public virtual IAsyncEnumerable<IReadOnlyCollection<RestMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | |||
| => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); | |||
| /// <inheritdoc /> | |||
| public Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null) | |||
| public virtual Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception> | |||
| public Task<RestUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, | |||
| public virtual Task<RestUserMessage> 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 | |||
| /// <exception cref="IOException">An I/O error occurred while opening the file.</exception> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception> | |||
| public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, | |||
| public virtual Task<RestUserMessage> 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 | |||
| /// <inheritdoc /> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception> | |||
| public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, | |||
| public virtual Task<RestUserMessage> 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 | |||
| /// <inheritdoc /> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception> | |||
| public Task<RestUserMessage> SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, | |||
| public virtual Task<RestUserMessage> 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 | |||
| /// <inheritdoc /> | |||
| /// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||
| /// <exception cref="ArgumentException">The only valid <see cref="MessageFlags"/> are <see cref="MessageFlags.SuppressEmbeds"/> and <see cref="MessageFlags.None"/>.</exception> | |||
| public Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> attachments, string text, bool isTTS = false, | |||
| public virtual Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> 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); | |||
| /// <inheritdoc /> | |||
| public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | |||
| public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) | |||
| => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); | |||
| /// <inheritdoc /> | |||
| public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) | |||
| public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) | |||
| => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); | |||
| /// <inheritdoc /> | |||
| public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | |||
| public virtual Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | |||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); | |||
| /// <inheritdoc /> | |||
| public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||
| public virtual Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||
| => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); | |||
| /// <inheritdoc /> | |||
| public async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null) | |||
| public virtual async Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> func, RequestOptions options = null) | |||
| => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| public Task TriggerTypingAsync(RequestOptions options = null) | |||
| public virtual Task TriggerTypingAsync(RequestOptions options = null) | |||
| => ChannelHelper.TriggerTypingAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| public IDisposable EnterTypingState(RequestOptions options = null) | |||
| public virtual IDisposable EnterTypingState(RequestOptions options = null) | |||
| => ChannelHelper.EnterTypingState(this, Discord, options); | |||
| /// <summary> | |||
| @@ -231,38 +238,6 @@ namespace Discord.Rest | |||
| public virtual Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetWebhooksAsync(this, Discord, options); | |||
| /// <summary> | |||
| /// Gets the parent (category) channel of this channel. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. The task result contains the category channel | |||
| /// representing the parent of this channel; <c>null</c> if none is set. | |||
| /// </returns> | |||
| public virtual Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| #endregion | |||
| #region Invites | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IInviteMetadata> 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<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IInviteMetadata> 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<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||
| => throw new NotImplementedException(); | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | |||
| /// <summary> | |||
| /// Creates a thread within this <see cref="ITextChannel"/>. | |||
| /// </summary> | |||
| @@ -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); | |||
| } | |||
| /// <summary> | |||
| /// Gets the parent (category) channel of this channel. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. The task result contains the category channel | |||
| /// representing the parent of this channel; <c>null</c> if none is set. | |||
| /// </returns> | |||
| public virtual Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| #endregion | |||
| #region Invites | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IInviteMetadata> 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<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IInviteMetadata> 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<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) | |||
| => throw new NotImplementedException(); | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | |||
| #endregion | |||
| #region ITextChannel | |||
| @@ -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. | |||
| /// </summary> | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel | |||
| public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel | |||
| { | |||
| #region RestVoiceChannel | |||
| /// <summary> | |||
| /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. | |||
| /// </summary> | |||
| public virtual bool IsTextInVoice | |||
| => Guild.Features.HasTextInVoice; | |||
| /// <inheritdoc /> | |||
| public int Bitrate { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int? UserLimit { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? CategoryId { get; private set; } | |||
| /// <inheritdoc/> | |||
| public string RTCRegion { get; private set; } | |||
| /// <inheritdoc /> | |||
| 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); | |||
| } | |||
| /// <summary> | |||
| /// Gets the parent (category) channel of this channel. | |||
| /// </summary> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous get operation. The task result contains the category channel | |||
| /// representing the parent of this channel; <c>null</c> if none is set. | |||
| /// </returns> | |||
| public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | |||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | |||
| /// <inheritdoc /> | |||
| public Task SyncPermissionsAsync(RequestOptions options = null) | |||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | |||
| #endregion | |||
| /// <inheritdoc/> | |||
| /// <exception cref="InvalidOperationException">Cannot modify text channel properties of a voice channel.</exception> | |||
| public override Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | |||
| => throw new InvalidOperationException("Cannot modify text channel properties of a voice channel"); | |||
| #region Invites | |||
| /// <inheritdoc /> | |||
| public async Task<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public async Task<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public virtual async Task<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public async Task<IInviteMetadata> 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); | |||
| /// <inheritdoc /> | |||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| /// <exception cref="InvalidOperationException">Cannot create a thread within a voice channel.</exception> | |||
| public override Task<RestThreadChannel> 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 | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestMessage> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) | |||
| { | |||
| if (!IsTextInVoice) | |||
| throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); | |||
| return base.DeleteMessagesAsync(messages, options); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null) | |||
| { | |||
| if (!IsTextInVoice) | |||
| throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); | |||
| return base.DeleteMessagesAsync(messageIds, options); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override IAsyncEnumerable<IReadOnlyCollection<RestMessage>> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<IReadOnlyCollection<RestMessage>> GetPinnedMessagesAsync(RequestOptions options = null) | |||
| { | |||
| if (!IsTextInVoice) | |||
| throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); | |||
| return base.GetPinnedMessagesAsync(options); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestWebhook> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null) | |||
| { | |||
| if (!IsTextInVoice) | |||
| throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); | |||
| return base.GetWebhooksAsync(options); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestWebhook> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<IUserMessage> ModifyMessageAsync(ulong messageId, Action<MessageProperties> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestUserMessage> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestUserMessage> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestUserMessage> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestUserMessage> SendFilesAsync(IEnumerable<FileAttachment> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| public override Task<RestUserMessage> 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); | |||
| } | |||
| /// <inheritdoc/> <exception cref="NotSupportedException">This function is only supported in Text-In-Voice channels.</exception> | |||
| 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 | |||
| /// <inheritdoc /> | |||
| /// <exception cref="NotSupportedException">Connecting to a REST-based channel is not supported.</exception> | |||
| @@ -1,5 +1,7 @@ | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Channel; | |||
| @@ -60,6 +62,33 @@ namespace Discord.Rest | |||
| return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | |||
| } | |||
| public static async Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) | |||
| { | |||
| var result = await client.ApiClient.GetActiveThreadsAsync(guild.Id, options).ConfigureAwait(false); | |||
| return result.Threads.Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray(); | |||
| } | |||
| public static async Task<IReadOnlyCollection<RestThreadChannel>> GetPublicArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, | |||
| DateTimeOffset? before = null, RequestOptions options = null) | |||
| { | |||
| var result = await client.ApiClient.GetPublicArchivedThreadsAsync(channel.Id, before, limit, options); | |||
| return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); | |||
| } | |||
| public static async Task<IReadOnlyCollection<RestThreadChannel>> GetPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, | |||
| DateTimeOffset? before = null, RequestOptions options = null) | |||
| { | |||
| var result = await client.ApiClient.GetPrivateArchivedThreadsAsync(channel.Id, before, limit, options); | |||
| return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); | |||
| } | |||
| public static async Task<IReadOnlyCollection<RestThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, | |||
| DateTimeOffset? before = null, RequestOptions options = null) | |||
| { | |||
| var result = await client.ApiClient.GetJoinedPrivateArchivedThreadsAsync(channel.Id, before, limit, options); | |||
| return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); | |||
| } | |||
| public static async Task<RestThreadUser[]> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | |||
| { | |||
| var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); | |||
| @@ -73,5 +102,114 @@ namespace Discord.Rest | |||
| return RestThreadUser.Create(client, channel.Guild, model, channel); | |||
| } | |||
| public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) | |||
| { | |||
| embeds ??= Array.Empty<Embed>(); | |||
| if (embed != null) | |||
| embeds = new[] { embed }.Concat(embeds).ToArray(); | |||
| Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); | |||
| Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); | |||
| Preconditions.AtMost(embeds.Length, 10, nameof(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 (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) | |||
| { | |||
| 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(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(allowedMentions)); | |||
| } | |||
| } | |||
| if (stickers != null) | |||
| { | |||
| Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); | |||
| } | |||
| if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) | |||
| throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); | |||
| var args = new CreatePostParams() | |||
| { | |||
| Title = title, | |||
| ArchiveDuration = archiveDuration, | |||
| Slowmode = slowmode, | |||
| Message = new() | |||
| { | |||
| AllowedMentions = allowedMentions.ToModel(), | |||
| Content = text, | |||
| Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified, | |||
| Flags = flags, | |||
| Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified, | |||
| Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional<ulong[]>.Unspecified, | |||
| } | |||
| }; | |||
| var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); | |||
| return RestThreadChannel.Create(client, channel.Guild, model); | |||
| } | |||
| public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) | |||
| { | |||
| embeds ??= Array.Empty<Embed>(); | |||
| if (embed != null) | |||
| embeds = new[] { embed }.Concat(embeds).ToArray(); | |||
| Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); | |||
| Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); | |||
| Preconditions.AtMost(embeds.Length, 10, nameof(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 (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) | |||
| { | |||
| 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(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(allowedMentions)); | |||
| } | |||
| } | |||
| if (stickers != null) | |||
| { | |||
| Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); | |||
| } | |||
| if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) | |||
| throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); | |||
| var args = new CreateMultipartPostAsync(attachments.ToArray()) | |||
| { | |||
| AllowedMentions = allowedMentions.ToModel(), | |||
| ArchiveDuration = archiveDuration, | |||
| Content = text, | |||
| Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional<API.Embed[]>.Unspecified, | |||
| Flags = flags, | |||
| MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified, | |||
| Slowmode = slowmode, | |||
| Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional<ulong[]>.Unspecified, | |||
| Title = title | |||
| }; | |||
| var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options); | |||
| return RestThreadChannel.Create(client, channel.Guild, model); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<ulong>.Unspecified, | |||
| @@ -1161,7 +1161,6 @@ namespace Discord.Rest | |||
| /// in order to use this property. | |||
| /// </remarks> | |||
| /// </param> | |||
| /// <param name="speakers">A collection of speakers for the event.</param> | |||
| /// <param name="location">The location of the event; links are supported</param> | |||
| /// <param name="coverImage">The optional banner image for the event.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| @@ -39,16 +39,16 @@ namespace Discord.Rest | |||
| { | |||
| } | |||
| internal new static async Task<RestCommandBase> CreateAsync(DiscordRestClient client, Model model) | |||
| internal new static async Task<RestCommandBase> 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); | |||
| } | |||
| /// <summary> | |||
| @@ -27,20 +27,20 @@ namespace Discord.Rest | |||
| { | |||
| } | |||
| internal static async Task<RestCommandBaseData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) | |||
| internal static async Task<RestCommandBaseData> 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<Model>(); | |||
| await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false); | |||
| await ResolvableData.PopulateAsync(client, guild, channel, model, doApiCall).ConfigureAwait(false); | |||
| } | |||
| } | |||
| @@ -22,7 +22,7 @@ namespace Discord.Rest | |||
| internal readonly Dictionary<ulong, Attachment> Attachments | |||
| = new Dictionary<ulong, Attachment>(); | |||
| 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; | |||
| @@ -20,22 +20,22 @@ namespace Discord.Rest | |||
| } | |||
| internal new static async Task<RestMessageCommand> CreateAsync(DiscordRestClient client, Model model) | |||
| internal new static async Task<RestMessageCommand> 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 | |||
| @@ -23,15 +23,15 @@ namespace Discord.Rest | |||
| /// <b>Note</b> Not implemented for <see cref="RestMessageCommandData"/> | |||
| /// </remarks> | |||
| public override IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options | |||
| => throw new System.NotImplementedException(); | |||
| => throw new NotImplementedException(); | |||
| internal RestMessageCommandData(DiscordRestClient client, Model model) | |||
| : base(client, model) { } | |||
| internal new static async Task<RestMessageCommandData> CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) | |||
| internal new static async Task<RestMessageCommandData> 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; | |||
| } | |||
| @@ -23,22 +23,22 @@ namespace Discord.Rest | |||
| { | |||
| } | |||
| internal new static async Task<RestUserCommand> CreateAsync(DiscordRestClient client, Model model) | |||
| internal new static async Task<RestUserCommand> 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 | |||