diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 84ee6e5a1..807381d31 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,3 @@ +github: quinchs open_collective: discordnet +custom: https://paypal.me/quinchs diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index e2c154130..29759facf 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -38,7 +38,7 @@ body: id: description attributes: label: Description - description: A brief explination of the bug. + description: A brief explanation of the bug. placeholder: When I start a DiscordSocketClient without stopping it, the gateway thread gets blocked. validations: required: true @@ -62,7 +62,7 @@ body: id: logs attributes: label: Logs - description: Add applicable logs and/or a stacktrace here. + description: Add applicable logs and/or a stack trace here. validations: required: true - type: textarea diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5547568..a4022e1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Discord.Net.targets b/Discord.Net.targets index e17f6de98..8cedb40e7 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,12 +1,12 @@ - 3.6.0 + 3.7.2 latest Discord.Net Contributors discord;discordapp https://github.com/Discord-Net/Discord.Net - http://opensource.org/licenses/MIT - https://github.com/Discord-Net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + MIT + PackageLogo.png git git://github.com/Discord-Net/Discord.Net @@ -23,4 +23,7 @@ true true + + + diff --git a/README.md b/README.md index bb8437432..e85216dbf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Discord

-Discord NET is an unofficial .NET API Wrapper for the Discord client (https://discord.com). +Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com). ## Documentation diff --git a/docs/docfx.json b/docs/docfx.json index 585d4dbec..5dd1e640d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.6.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.7.2", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/guides/int_basics/modals/intro.md b/docs/guides/int_basics/modals/intro.md index 81f0da03c..3e738c6d8 100644 --- a/docs/guides/int_basics/modals/intro.md +++ b/docs/guides/int_basics/modals/intro.md @@ -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 diff --git a/docs/guides/int_framework/autocompletion.md b/docs/guides/int_framework/autocompletion.md index 834db2b4f..27da54e36 100644 --- a/docs/guides/int_framework/autocompletion.md +++ b/docs/guides/int_framework/autocompletion.md @@ -18,6 +18,8 @@ AutocompleteHandlers raise the `AutocompleteHandlerExecuted` event on execution. A valid AutocompleteHandlers must inherit [AutocompleteHandler] base type and implement all of its abstract methods. +[!code-csharp[Autocomplete Command Example](samples/autocompletion/autocomplete-example.cs)] + ### GenerateSuggestionsAsync() The Interactions Service uses this method to generate a response of an Autocomplete Interaction. diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index c019b1424..54e9086a1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types: - Implementations of [IChannel] - Implementations of [IRole] - Implementations of [IMentionable] +- Implementations of [IAttachment] - `string` - `float`, `double`, `decimal` - `bool` diff --git a/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs new file mode 100644 index 000000000..30c0697e1 --- /dev/null +++ b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs @@ -0,0 +1,20 @@ +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete(typeof(ExampleAutocompleteHandler))] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); + +public class ExampleAutocompleteHandler : AutocompleteHandler +{ + public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + { + // Create a collection with suggestions for autocomplete + IEnumerable results = new[] + { + new AutocompleteResult("Name1", "value111"), + new AutocompleteResult("Name2", "value2") + }; + + // max - 25 suggestions at a time (API limit) + return AutocompletionResult.FromSuccess(results.Take(25)); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/autocomplete.cs b/docs/guides/int_framework/samples/intro/autocomplete.cs index f93c56eaa..11de489f1 100644 --- a/docs/guides/int_framework/samples/intro/autocomplete.cs +++ b/docs/guides/int_framework/samples/intro/autocomplete.cs @@ -1,9 +1,21 @@ [AutocompleteCommand("parameter_name", "command_name")] public async Task Autocomplete() { - IEnumerable results; + string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString(); - ... + IEnumerable results = new[] + { + new AutocompleteResult("foo", "foo_value"), + new AutocompleteResult("bar", "bar_value"), + new AutocompleteResult("baz", "baz_value"), + }.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching - await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results); + + // max - 25 suggestions at a time + await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25)); } + +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); \ No newline at end of file diff --git a/samples/BasicBot/_BasicBot.csproj b/samples/BasicBot/_BasicBot.csproj index 6e1a6365f..e6245d340 100644 --- a/samples/BasicBot/_BasicBot.csproj +++ b/samples/BasicBot/_BasicBot.csproj @@ -1,12 +1,12 @@ - + Exe - net5.0 + net6.0 - + diff --git a/samples/InteractionFramework/_InteractionFramework.csproj b/samples/InteractionFramework/_InteractionFramework.csproj index f11c2bd3d..8892a65b7 100644 --- a/samples/InteractionFramework/_InteractionFramework.csproj +++ b/samples/InteractionFramework/_InteractionFramework.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 InteractionFramework @@ -13,13 +13,7 @@ - - - - - - - + diff --git a/samples/ShardedClient/_ShardedClient.csproj b/samples/ShardedClient/_ShardedClient.csproj index 69576ea27..68a43c7cd 100644 --- a/samples/ShardedClient/_ShardedClient.csproj +++ b/samples/ShardedClient/_ShardedClient.csproj @@ -2,18 +2,13 @@ Exe - net5.0 + net6.0 ShardedClient - - - - - - + diff --git a/samples/TextCommandFramework/_TextCommandFramework.csproj b/samples/TextCommandFramework/_TextCommandFramework.csproj index ee64205f5..6e00625e8 100644 --- a/samples/TextCommandFramework/_TextCommandFramework.csproj +++ b/samples/TextCommandFramework/_TextCommandFramework.csproj @@ -2,17 +2,14 @@ Exe - net5.0 + net6.0 TextCommandFramework - - - - - + + diff --git a/samples/WebhookClient/_WebhookClient.csproj b/samples/WebhookClient/_WebhookClient.csproj index 91131894d..515fcf3a4 100644 --- a/samples/WebhookClient/_WebhookClient.csproj +++ b/samples/WebhookClient/_WebhookClient.csproj @@ -2,12 +2,12 @@ Exe - net5.0 + net6.0 WebHookClient - + diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index fea719016..4fdecd254 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -7,6 +7,8 @@ A Discord.Net extension adding support for bot commands. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs index fb266efa6..5b9bfe72b 100644 --- a/src/Discord.Net.Commands/Results/MatchResult.cs +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord.Commands { @@ -12,7 +12,7 @@ namespace Discord.Commands /// /// Gets on which pipeline stage the command may have matched or failed. /// - public IResult? Pipeline { get; } + public IResult Pipeline { get; } /// public CommandError? Error { get; } @@ -21,7 +21,7 @@ namespace Discord.Commands /// public bool IsSuccess => !Error.HasValue; - private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, string errorReason) { Match = match; Error = error; diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 783565e04..41d83bbc8 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -7,6 +7,8 @@ The core components for the Discord.Net library. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 067c55225..2db802f1e 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -132,6 +132,16 @@ namespace Discord /// public const int MaxAuditLogEntriesPerBatch = 100; + /// + /// Returns the max number of stickers that can be sent with a message. + /// + public const int MaxStickersPerMessage = 3; + + /// + /// Returns the max number of embeds that can be sent with a message. + /// + public const int MaxEmbedsPerMessage = 10; + /// /// Gets or sets how a request should act in the case of an error, by default. /// diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 51fd736f6..b444614e4 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -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 diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs index e60bd5031..15965abc3 100644 --- a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -26,6 +26,8 @@ namespace Discord /// The channel is a stage voice channel. Stage = 13, /// The channel is a guild directory used in hub servers. (Unreleased) - GuildDirectory = 14 + GuildDirectory = 14, + /// The channel is a forum channel containing multiple threads. + Forum = 15 } } diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs new file mode 100644 index 000000000..f4c6da2e2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -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 + { + /// + /// Gets a value that indicates whether the channel is NSFW. + /// + /// + /// true if the channel has the NSFW flag enabled; otherwise false. + /// + bool IsNsfw { get; } + + /// + /// Gets the current topic for this text channel. + /// + /// + /// A string representing the topic set in the channel; null if none is set. + /// + string Topic { get; } + + /// + /// Gets the default archive duration for a newly created post. + /// + ThreadArchiveDuration DefaultAutoArchiveDuration { get; } + + /// + /// Gets a collection of tags inside of this forum channel. + /// + IReadOnlyCollection Tags { get; } + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + Task 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The file path of the file. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + Task 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The of the file to be sent. + /// The name of the attachment. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The attachment containing the file and description. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// A collection of attachments to upload. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable 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); + + /// + /// Gets a collection of active threads within this forum channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of active threads. + /// + Task> GetActiveThreadsAsync(RequestOptions options = null); + + /// + /// Gets a collection of publicly archived threads within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of publicly archived threads. + /// + Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads within this forum channel. + /// + /// + /// The bot requires the permission in order to execute this request. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads that the current bot has joined within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index ae0fe674b..af4e5ec6a 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -35,6 +35,17 @@ namespace Discord /// int SlowModeInterval { get; } + /// + /// Gets the default auto-archive duration for client-created threads in this channel. + /// + /// + /// The value of this property does not affect API thread creation, it will not respect this value. + /// + /// + /// The default auto-archive duration for thread creation in this channel. + /// + ThreadArchiveDuration DefaultArchiveDuration { get; } + /// /// Bulk-deletes multiple messages. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 1d36a41b9..d921a2474 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -6,7 +6,7 @@ namespace Discord /// /// Represents a generic voice channel in a guild. /// - public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable + public interface IVoiceChannel : IMessageChannel, INestedChannel, IAudioChannel, IMentionable { /// /// Gets the bit-rate that the clients in this voice channel are requested to use. diff --git a/src/Discord.Net.Core/Entities/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTag.cs new file mode 100644 index 000000000..26ae4301e --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTag.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A struct representing a forum channel tag. + /// + public struct ForumTag + { + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + /// Gets the name of the tag. + /// + public string Name { get; } + + /// + /// Gets the emoji of the tag or if none is set. + /// + 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; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 4706b629e..775ff9e65 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1173,7 +1173,6 @@ namespace Discord /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index 4b2fa3bee..7219682b7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -89,7 +89,7 @@ namespace Discord /// Gets this events banner image url. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// The cover images url. string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 5bb00797b..4506b66d9 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -56,7 +56,7 @@ namespace Discord Number = 10, /// - /// A . + /// A . /// Attachment = 11 } diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 8f6bef995..a2dbe0e5f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -52,10 +52,13 @@ namespace Discord /// /// Gets the preferred locale of the invoking User. /// + /// + /// This property returns if the interaction is a REST ping interaction. + /// string UserLocale { get; } /// - /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. + /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. /// /// /// Non-community guilds (With no locale setting available) will have en-US as the default value sent by Discord. @@ -67,6 +70,27 @@ namespace Discord /// bool IsDMInteraction { get; } + /// + /// Gets the ID of the channel this interaction was executed in. + /// + /// + /// This property returns if the interaction is a REST ping interaction. + /// + ulong? ChannelId { get; } + + /// + /// Gets the ID of the guild this interaction was executed in. + /// + /// + /// This property returns if the interaction was not executed in a guild. + /// + ulong? GuildId { get; } + + /// + /// Gets the ID of the application this interaction is for. + /// + ulong ApplicationId { get; } + /// /// Responds to an Interaction with type . /// diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 7becca0e0..37342b039 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -195,7 +195,7 @@ namespace Discord /// /// The button to add. /// The row to add the button. - /// There is no more row to add a menu. + /// There is no more row to add a button. /// must be less than . /// The current builder. public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) @@ -348,6 +348,100 @@ namespace Discord return this; } + /// + /// Adds a to the . + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled)); + } + + /// + /// Adds a to the . + /// + /// The menu to add. + /// A Select Menu cannot exist in a pre-occupied ActionRow. + /// The current builder. + public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) + { + if (menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (Components.Count != 0) + throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); + + AddComponent(builtMenu); + + return this; + } + + /// + /// Adds a with specified parameters to the . + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The current builder. + public ActionRowBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled); + + return WithButton(button); + } + + /// + /// Adds a to the . + /// + /// The button to add. + /// Components count reached . + /// A button cannot be added to a row with a SelectMenu. + /// The current builder. + public ActionRowBuilder WithButton(ButtonBuilder button) + { + var builtButton = button.Build(); + + if(Components.Count >= 5) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); + + AddComponent(builtButton); + + return this; + } + /// /// Builds the current builder to a that can be used within a /// @@ -1194,9 +1288,9 @@ namespace Discord /// /// Gets or sets the default value of the text input. /// - /// is less than 0. + /// .Length is less than 0. /// - /// is greater than or . + /// .Length is greater than or . /// public string Value { @@ -1227,7 +1321,7 @@ namespace Discord /// The text input's minimum length. /// The text input's maximum length. /// The text input's required value. - public TextInputBuilder (string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, + public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int? minLength = null, int? maxLength = null, bool? required = null, string value = null) { Label = label; @@ -1291,7 +1385,7 @@ namespace Discord Placeholder = placeholder; return this; } - + /// /// Sets the value of the current builder. /// @@ -1306,18 +1400,18 @@ namespace Discord /// /// Sets the minimum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMinLength(int minLength) { MinLength = minLength; return this; } - + /// /// Sets the maximum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMaxLength(int maxLength) { diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs index 3a3e3cc49..817f69415 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs @@ -64,18 +64,18 @@ namespace Discord /// /// Sets the custom id of the current modal. /// - /// The value to set the custom id to. + /// The value to set the custom id to. /// The current builder. public ModalBuilder WithCustomId(string customId) { CustomId = customId; return this; } - + /// /// Adds a component to the current builder. /// - /// The component to add. + /// The component to add. /// The current builder. public ModalBuilder AddTextInput(TextInputBuilder component) { @@ -213,7 +213,7 @@ namespace Discord /// Adds a to the at the specific row. /// If the row cannot accept the component then it will add it to a row that can. /// - /// The to add. + /// The to add. /// The row to add the text input. /// There are no more rows to add a text input to. /// must be less than . diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index bf74a160c..d7d086762 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -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(); if (Options.Count + options.Length > MaxOptionsCount) @@ -409,7 +406,7 @@ namespace Discord MinValue = MinValue, MaxValue = MaxValue }; - } + } /// /// Adds an option to the current slash command. @@ -477,6 +474,26 @@ namespace Discord return this; } + /// + /// Adds a collection of options to the current option. + /// + /// The collection of options to add. + /// The current builder. + 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; + } + /// /// Adds a choice to the current option. /// @@ -640,7 +657,7 @@ namespace Discord MinValue = value; return this; } - + /// /// Sets the current builders max value field. /// diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 0304120f5..1e2a7b0d7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Discord.Utils; +using Newtonsoft.Json; namespace Discord { @@ -155,6 +156,55 @@ namespace Discord } } + /// + /// Tries to parse a string into an . + /// + /// The json string to parse. + /// The with populated values. An empty instance if method returns . + /// if was succesfully parsed. if not. + public static bool TryParse(string json, out EmbedBuilder builder) + { + builder = new EmbedBuilder(); + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + { + builder = model.ToEmbedBuilder(); + return true; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Parses a string into an . + /// + /// The json string to parse. + /// An with populated values from the passed . + /// Thrown if the string passed is not valid json. + public static EmbedBuilder Parse(string json) + { + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + return model.ToEmbedBuilder(); + + return new EmbedBuilder(); + } + catch + { + throw; + } + } + /// /// Sets the title of an . /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs index 029910e56..7fdc448ad 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -27,6 +27,12 @@ namespace Discord /// public Optional GuildId { get; internal set; } + /// + /// Gets whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message + /// Defaults to true. + /// + public Optional FailIfNotExists { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -39,16 +45,21 @@ namespace Discord /// /// The ID of the guild that will be referenced. It will be validated if sent. /// - public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null) + /// + /// Whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message. Defaults to true. + /// + public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null, bool? failIfNotExists = null) { MessageId = messageId ?? Optional.Create(); InternalChannelId = channelId ?? Optional.Create(); GuildId = guildId ?? Optional.Create(); + FailIfNotExists = failIfNotExists ?? Optional.Create(); } private string DebuggerDisplay => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + - $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}" + + $"{(FailIfNotExists.IsSpecified ? $", FailIfNotExists: ({FailIfNotExists.Value})" : "")}"; public override string ToString() => DebuggerDisplay; diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index ee5c9984a..3c6a804c5 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,30 +7,55 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - /// Gets a blank that grants no permissions. - /// A structure that does not contain any set permissions. - public static readonly ChannelPermissions None = new ChannelPermissions(); - /// Gets a that grants all permissions for text channels. - public static readonly ChannelPermissions Text = new ChannelPermissions(0b0_11111_0101100_0000000_1111111110001_010001); - /// Gets a that grants all permissions for voice channels. - public static readonly ChannelPermissions Voice = new ChannelPermissions(0b1_00000_0000100_1111110_0000000011100_010001); - /// Gets a that grants all permissions for stage channels. - public static readonly ChannelPermissions Stage = new ChannelPermissions(0b0_00000_1000100_0111010_0000000010000_010001); - /// Gets a that grants all permissions for category channels. - public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); - /// Gets a that grants all permissions for direct message channels. - public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000); - /// Gets a that grants all permissions for group channels. - public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); - /// Gets a that grants all permissions for a given channel type. + /// + /// Gets a blank that grants no permissions. + /// + /// + /// A structure that does not contain any set permissions. + /// + public static readonly ChannelPermissions None = new(); + + /// + /// Gets a that grants all permissions for text channels. + /// + public static readonly ChannelPermissions Text = new(0b0_11111_0101100_0000000_1111111110001_010001); + + /// + /// Gets a that grants all permissions for voice channels. + /// + public static readonly ChannelPermissions Voice = new(0b1_11111_0101100_1111110_1111111111101_010001); // (0b1_00000_0000100_1111110_0000000011100_010001 (<- voice only perms) |= Text) + + /// + /// Gets a that grants all permissions for stage channels. + /// + public static readonly ChannelPermissions Stage = new(0b0_00000_1000100_0111010_0000000010000_010001); + + /// + /// Gets a that grants all permissions for category channels. + /// + public static readonly ChannelPermissions Category = new(0b01100_1111110_1111111110001_010001); + + /// + /// Gets a that grants all permissions for direct message channels. + /// + public static readonly ChannelPermissions DM = new(0b00000_1000110_1011100110001_000000); + + /// + /// Gets a that grants all permissions for group channels. + /// + public static readonly ChannelPermissions Group = new(0b00000_1000110_0001101100000_000000); + + /// + /// Gets a that grants all permissions for a given channel type. + /// /// Unknown channel type. public static ChannelPermissions All(IChannel channel) { return channel switch { - ITextChannel _ => Text, IStageChannel _ => Stage, IVoiceChannel _ => Voice, + ITextChannel _ => Text, ICategoryChannel _ => Category, IDMChannel _ => DM, IGroupChannel _ => Group, diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 935b956c3..5411f5ebf 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -79,7 +79,7 @@ namespace Discord /// Sets a timestamp how long a user should be timed out for. /// /// - /// or a time in the past to clear a currently existing timeout. + /// or a time in the past to clear a currently existing timeout. /// public Optional TimedOutUntil { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 96de06ed8..9703eafe7 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -104,7 +104,7 @@ namespace Discord /// Gets the date and time that indicates if and for how long a user has been timed out. /// /// - /// or a timestamp in the past if the user is not timed out. + /// or a timestamp in the past if the user is not timed out. /// /// /// A indicating how long the user will be timed out for. @@ -116,7 +116,7 @@ namespace Discord /// /// /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); @@ -151,7 +151,7 @@ namespace Discord /// If the user does not have a guild avatar, this will be the user's regular avatar. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// /// A string representing the URL of the displayed avatar for this user. if the user does not have an avatar in place. /// diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index dc2a06540..d9ad43f0d 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -37,8 +37,9 @@ namespace Discord /// Sanitizes the string, safely escaping any Markdown sequences. public static string Sanitize(string text) { - foreach (string unsafeChar in SensitiveCharacters) - text = text.Replace(unsafeChar, $"\\{unsafeChar}"); + if (text != null) + foreach (string unsafeChar in SensitiveCharacters) + text = text.Replace(unsafeChar, $"\\{unsafeChar}"); return text; } diff --git a/src/Discord.Net.Core/Utils/UrlValidation.cs b/src/Discord.Net.Core/Utils/UrlValidation.cs index 8e877bd4e..55ae3bdf7 100644 --- a/src/Discord.Net.Core/Utils/UrlValidation.cs +++ b/src/Discord.Net.Core/Utils/UrlValidation.cs @@ -23,7 +23,7 @@ namespace Discord.Utils /// /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord - /// should be used everything other than url buttons. + /// should be used everything other than url buttons. /// /// The URL to validate before sending to discord. /// A URL must include a protocol (either http, https, or discord). diff --git a/src/Discord.Net.Examples/Discord.Net.Examples.csproj b/src/Discord.Net.Examples/Discord.Net.Examples.csproj index b4a336f9f..1bdca7992 100644 --- a/src/Discord.Net.Examples/Discord.Net.Examples.csproj +++ b/src/Discord.Net.Examples/Discord.Net.Examples.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs index e17c9ff14..c8a3428db 100644 --- a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Set the to . + /// Set the to . /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class AutocompleteAttribute : Attribute @@ -14,7 +14,7 @@ namespace Discord.Interactions public Type AutocompleteHandlerType { get; } /// - /// Set the to and define a to handle + /// Set the to and define a to handle /// Autocomplete interactions targeting the parameter this is applied to. /// /// @@ -29,7 +29,7 @@ namespace Discord.Interactions } /// - /// Set the to without specifying a . + /// Set the to without specifying a . /// public AutocompleteAttribute() { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index d611b574d..e9b877268 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -21,9 +21,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// The label of the input. /// The custom id of the input. - /// Whether the user is required to input a value.> protected ModalInputAttribute(string customId) { CustomId = customId; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 35121cd6b..4439e1d84 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -36,7 +36,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// + /// The custom id of the text input.> /// The style of the text input. /// The placeholder of the text input. /// The minimum length of the text input's content. diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 77d6e8f25..0f6ecfc66 100644 --- a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -29,7 +29,7 @@ namespace Discord.Interactions /// /// This precondition will always fail if the command is being invoked in a . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// @@ -41,7 +41,7 @@ namespace Discord.Interactions /// /// Requires that the user invoking the command to have a specific . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs index cd9bdfc24..c21fd5ae8 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs @@ -56,7 +56,7 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 340119ddd..8dd2c4004 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index fc1dbdc0e..c13ff40de 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders } /// - /// Adds text components to . + /// Adds text components to . /// /// Text Component builder factory. /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index b7f00025f..0eb91ee6a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -357,7 +357,8 @@ namespace Discord.Interactions.Builders return this; } - + + /// /// Adds a modal command builder to . /// /// factory. diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs index 78d007d44..fec1a6ce9 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs @@ -122,7 +122,7 @@ namespace Discord.Interactions.Builders /// /// Adds preconditions to /// - /// New attributes to be added to . + /// New attributes to be added to . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj index c617eff61..a3ac3d508 100644 --- a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj +++ b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj @@ -7,8 +7,10 @@ Discord.Interactions Discord.Net.Interactions A Discord.Net extension adding support for Application Commands. + 5 + True - + diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 904d67410..4f40f1607 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -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; } diff --git a/src/Discord.Net.Interactions/InteractionContext.cs b/src/Discord.Net.Interactions/InteractionContext.cs index 024ab5ef8..b81cc5938 100644 --- a/src/Discord.Net.Interactions/InteractionContext.cs +++ b/src/Discord.Net.Interactions/InteractionContext.cs @@ -24,8 +24,7 @@ namespace Discord.Interactions /// /// The underlying client. /// The underlying interaction. - /// who executed the command. - /// the command originated from. + /// the command originated from. public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IMessageChannel channel = null) { Client = client; diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs index 873f4c173..a14779dbb 100644 --- a/src/Discord.Net.Interactions/InteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs @@ -45,7 +45,7 @@ namespace Discord.Interactions protected virtual async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => await Context.Interaction.DeferAsync(ephemeral, options).ConfigureAwait(false); - /// + /// protected virtual async Task RespondAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -70,7 +70,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task FollowupAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -95,7 +95,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task ReplyAsync (string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null) => await Context.Channel.SendMessageAsync(text, false, embed, options, allowedMentions, messageReference, components).ConfigureAwait(false); @@ -118,9 +118,9 @@ namespace Discord.Interactions /// protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal); - /// - protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where T : class, IModal - => await Context.Interaction.RespondWithModalAsync(customId, options); + /// + protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where TModal : class, IModal + => await Context.Interaction.RespondWithModalAsync(customId, options); //IInteractionModuleBase diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 8eb5799d6..793d89cdc 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -223,7 +223,8 @@ namespace Discord.Interactions new ConcurrentDictionary { [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) }); _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), @@ -234,7 +235,8 @@ namespace Discord.Interactions [typeof(IUser)] = typeof(DefaultUserReader<>), [typeof(IMessage)] = typeof(DefaultMessageReader<>), [typeof(IConvertible)] = typeof(DefaultValueReader<>), - [typeof(Enum)] = typeof(EnumReader<>) + [typeof(Enum)] = typeof(EnumReader<>), + [typeof(Nullable<>)] = typeof(NullableReader<>) }); } @@ -421,20 +423,39 @@ namespace Discord.Interactions /// /// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// /// The target guild. + /// If , this operation will not delete the commands that are missing from . /// Commands to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> 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); + } + + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) + { + EnsureClientReady(); + var props = new List(); 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); } /// /// Register Application Commands from modules provided in to a guild. /// /// The target guild. + /// If , this operation will not delete the commands that are missing from . /// Modules to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> 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); + } + + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> 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); } /// /// Register Application Commands from modules provided in as global commands. /// + /// If , this operation will not delete the commands that are missing from . /// Modules to be registered to Discord. /// /// 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 /// /// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// + /// If , this operation will not delete the commands that are missing from . /// Commands to be registered to Discord. /// /// 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()); } internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) @@ -960,7 +1003,7 @@ namespace Discord.Interactions /// Removes a type reader for the given type. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -973,7 +1016,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the type . /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the readers from. @@ -986,7 +1029,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the given type. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -999,7 +1042,7 @@ namespace Discord.Interactions /// Serialize an object using a into a to be placed in a Component CustomId. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// Type of the object to be serialized. @@ -1079,19 +1122,40 @@ namespace Discord.Interactions /// /// The active command permissions after the modification. /// - public async Task ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild, + public async Task 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); + } + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task 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 /// /// The active command permissions after the modification. /// - public async Task ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) => - await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); + public async Task 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); + } + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); /// /// Modify the command permissions of the matching Discord Slash Command. @@ -1119,20 +1203,40 @@ namespace Discord.Interactions /// /// The active command permissions after the modification. /// - public async Task ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) => - await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); + public async Task 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 ModifyApplicationCommandPermissionsAsync (T command, IGuild guild, + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); + + private async Task ModifyApplicationCommandPermissionsAsync (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); diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index e83c91fef..b570e6d84 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -87,12 +87,12 @@ namespace Discord.Interactions await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } - protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) + protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) { if (Context.Interaction is not RestInteraction restInteraction) throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); - var payload = restInteraction.RespondWithModal(customId, options); + var payload = restInteraction.RespondWithModal(customId, options); if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); diff --git a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs index bd89bf6b7..a9a12ee33 100644 --- a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs +++ b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Represents a result type for . + /// Represents a result type for . /// public struct TypeConverterResult : IResult { diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs new file mode 100644 index 000000000..ba6568ad1 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableComponentConverter : ComponentTypeConverter + { + private readonly ComponentTypeConverter _typeConverter; + + public NullableComponentConverter(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeConverter = interactionService.GetComponentTypeConverter(type, services); + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs new file mode 100644 index 000000000..ed88dc64a --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableReader : TypeReader + { + private readonly TypeReader _typeReader; + + public NullableReader(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeReader = interactionService.GetTypeReader(type, services); + } + + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + => string.IsNullOrEmpty(option) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeReader.ReadAsync(context, option, services); + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 60980c065..e4b6f893c 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -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; } } diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index d565b269a..d9d7d469c 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -66,5 +66,12 @@ namespace Discord.API [JsonProperty("member_count")] public Optional MemberCount { get; set; } + + //ForumChannel + [JsonProperty("available_tags")] + public Optional ForumTags { get; set; } + + [JsonProperty("default_auto_archive_duration")] + public Optional AutoArchiveDuration { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs index 94b2396bf..9fa3e38ce 100644 --- a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs +++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs @@ -9,8 +9,5 @@ namespace Discord.API.Rest [JsonProperty("members")] public ThreadMember[] Members { get; set; } - - [JsonProperty("has_more")] - public bool HasMore { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ForumTags.cs b/src/Discord.Net.Rest/API/Common/ForumTags.cs new file mode 100644 index 000000000..18354e7b2 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumTags.cs @@ -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 EmojiId { get; set; } + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs new file mode 100644 index 000000000..132e38e5f --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs @@ -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 Content { get; set; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("sticker_ids")] + public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs index 6cc7603e0..70ef4e678 100644 --- a/src/Discord.Net.Rest/API/Common/MessageReference.cs +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -12,5 +12,8 @@ namespace Discord.API [JsonProperty("guild_id")] public Optional GuildId { get; set; } + + [JsonProperty("fail_if_not_exists")] + public Optional FailIfNotExists { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs new file mode 100644 index 000000000..0c8bc5494 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -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 Slowmode { get; set; } + + + public Optional Content { get; set; } + public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageComponent { get; set; } + public Optional Flags { get; set; } + public Optional Stickers { get; set; } + + public CreateMultipartPostAsync(params FileAttachment[] attachments) + { + Files = attachments; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + + var payload = new Dictionary(); + var message = new Dictionary(); + + 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 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.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; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs new file mode 100644 index 000000000..974e07c0a --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -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 Slowmode { get; set; } + + [JsonProperty("message")] + public ForumThreadMessage Message { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 67a690e4d..b85ff646e 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -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) diff --git a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs index f004dec82..ca0f49ccb 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs @@ -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) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 1a25e4782..d945d149b 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -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) diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 837fd1d04..59e1f0b4b 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Integration")] [assembly: InternalsVisibleTo("Discord.Net.Interactions")] [assembly: TypeForwardedTo(typeof(Discord.Embed))] diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 98692998f..bec2396ef 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -7,6 +7,8 @@ A core Discord.Net library containing the REST client and models. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3b829ee17..e179675ba 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -173,10 +173,12 @@ namespace Discord.API private async Task LogoutInternalAsync() { //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. - if (LoginState == LoginState.LoggedOut) return; + if (LoginState == LoginState.LoggedOut) + return; LoginState = LoginState.LoggingOut; - try { _loginCancelToken?.Cancel(false); } + try + { _loginCancelToken?.Cancel(false); } catch { } await DisconnectInternalAsync(null).ConfigureAwait(false); @@ -398,7 +400,7 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); @@ -414,9 +416,9 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); - if(args.Topic.IsSpecified) + if (args.Topic.IsSpecified) Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name)); Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval)); @@ -464,6 +466,24 @@ namespace Discord.API #endregion #region Threads + public async Task CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options); + } + + public async Task CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendMultipartAsync("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options); + } + public async Task ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -564,15 +584,15 @@ namespace Discord.API return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); } - public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) + public async Task 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("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); + return await SendAsync("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options); } public async Task 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("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -816,12 +840,12 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null) + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -837,11 +861,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// This operation may only be called with a token. - public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null) + public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -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); } /// Message content is too long, length must be less or equal to . @@ -873,7 +897,7 @@ namespace Discord.API /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -893,7 +917,7 @@ namespace Discord.API } var ids = new BucketIds(webhookId: webhookId); - return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -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("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 querys = new List() { }; + if (wait) + querys.Add("wait=true"); + if (threadId.HasValue) + querys.Add($"thread_id={threadId}"); + + return $"{string.Join("&", querys)}"; + } + #endregion } } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b1948f80a..daf7287c7 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -32,9 +32,15 @@ namespace Discord.Rest /// Initializes a new with the provided configuration. /// /// The configuration to be used with the client. - public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } // used for socket client rest access - internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, serializer: Serializer, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); @@ -82,6 +88,8 @@ namespace Discord.Rest #region Rest interactions + private readonly bool _apiOnCreation; + public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, string body) => IsValidHttpInteraction(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, byte[] body) @@ -113,8 +121,8 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body) - => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); + public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func doApiCallOnCreation = null) + => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation); /// /// Creates a from a http message. @@ -127,7 +135,7 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body) + public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func doApiCallOnCreation = null) { if (!IsValidHttpInteraction(publicKey, signature, timestamp, body)) { @@ -138,12 +146,12 @@ namespace Discord.Rest using (var jsonReader = new JsonTextReader(textReader)) { var model = Serializer.Deserialize(jsonReader); - return await RestInteraction.CreateAsync(this, model); + return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation is not null ? doApiCallOnCreation(new InteractionProperties(model)) : _apiOnCreation); } } #endregion - + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 7bf7440ce..a09d9ee98 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -9,5 +9,7 @@ namespace Discord.Rest { /// Gets or sets the provider used to generate new REST connections. public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; + + public bool APIOnRestInteractionCreation { get; set; } = true; } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index fc807cac0..7246ac197 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BanAuditLogData(RestUser.Create(discord, userInfo)); + return new BanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was banned. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the banned user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs index 0d12e4609..288cb9d0a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + return new BotAddAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the bot that was added. /// + /// + /// Will be if the bot is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the bot. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index b177b2435..3560b9a27 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 9d0aed12b..2dc2f22f6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs index dceb73d0a..b533f0268 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,12 +18,15 @@ namespace Discord.Rest internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new KickAuditLogData(RestUser.Create(discord, userInfo)); + return new KickAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was kicked. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the kicked user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs index 763c90c68..276604d03 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -27,7 +27,7 @@ namespace Discord.Rest .ToList(); var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, userInfo); + RestUser user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs index f22b83e4c..f3437e621 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -33,7 +33,7 @@ namespace Discord.Rest newMute = muteModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, targetInfo); + RestUser user = (targetInfo != null) ? RestUser.Create(discord, targetInfo) : null; var before = new MemberInfo(oldNick, oldDeaf, oldMute); var after = new MemberInfo(newNick, newDeaf, newMute); @@ -44,6 +44,9 @@ namespace Discord.Rest /// /// Gets the user that the changes were performed on. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user who the changes were performed on. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index 66b3f7d83..746fc2ea6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -2,6 +2,7 @@ using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; +using System; namespace Discord.Rest { @@ -20,7 +21,7 @@ namespace Discord.Rest internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, userInfo != null ? RestUser.Create(discord, userInfo) : null); } /// @@ -41,6 +42,9 @@ namespace Discord.Rest /// /// Gets the user of the messages that were deleted. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the deleted messages. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs index be66ac846..c33fd5f44 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was pinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the pinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs index b4fa389cc..f6fd31771 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was unpinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the unpinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs index bc7e7fd4f..f12d9a1af 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,7 +18,7 @@ namespace Discord.Rest internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); + return new UnbanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs new file mode 100644 index 000000000..aff8400aa --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -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 +{ + /// + /// Represents a REST-based forum channel in a guild. + /// + public class RestForumChannel : RestGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + 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()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + public Task 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); + + /// + public async Task 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); + } + + /// + public async Task 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); + } + + /// + public Task 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); + + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable 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); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task 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 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 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 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 IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable 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 + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index fa2362854..4f9af0335 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -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), diff --git a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs index c01df96fd..b34afd027 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs @@ -12,7 +12,11 @@ namespace Discord.Rest public class RestStageChannel : RestVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -37,13 +41,11 @@ namespace Discord.Rest IsLive = isLive; if(isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 76c75ab6e..81f21bcd7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -21,11 +21,12 @@ namespace Discord.Rest public virtual int SlowModeInterval { get; private set; } /// public ulong? CategoryId { get; private set; } - /// public string Mention => MentionUtils.MentionChannel(Id); /// public bool IsNsfw { get; private set; } + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) @@ -46,6 +47,12 @@ namespace Discord.Rest if (model.SlowMode.IsSpecified) SlowModeInterval = model.SlowMode.Value; IsNsfw = model.Nsfw.GetValueOrDefault(); + + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// @@ -86,25 +93,25 @@ namespace Discord.Rest => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); /// - public Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); /// - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); /// - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -136,7 +143,7 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -146,7 +153,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -156,7 +163,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -166,35 +173,35 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); /// @@ -231,38 +238,6 @@ namespace Discord.Rest public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public virtual Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion - - #region Invites - /// - public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - /// - public virtual async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - /// /// Creates a thread within this . /// @@ -299,6 +274,38 @@ namespace Discord.Rest var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); return RestThreadChannel.Create(Discord, Guild, model); } + + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public virtual Task GetCategoryAsync(RequestOptions options = null) + => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion + + #region Invites + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; #endregion #region ITextChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index bcf03a5bc..31d313a48 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -2,6 +2,7 @@ using Discord.Audio; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -12,21 +13,21 @@ namespace Discord.Rest /// Represents a REST-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel + public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel { #region RestVoiceChannel + /// + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. + /// + public virtual bool IsTextInVoice + => Guild.Features.HasTextInVoice; /// public int Bitrate { get; private set; } /// public int? UserLimit { get; private set; } - /// - public ulong? CategoryId { get; private set; } /// public string RTCRegion { get; private set; } - /// - public string Mention => MentionUtils.MentionChannel(Id); - internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -41,7 +42,6 @@ namespace Discord.Rest internal override void Update(Model model) { base.Update(model); - CategoryId = model.CategoryId; if(model.Bitrate.IsSpecified) Bitrate = model.Bitrate.Value; @@ -59,41 +59,185 @@ namespace Discord.Rest Update(model); } - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion + /// + /// Cannot modify text channel properties of a voice channel. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties of a voice channel"); - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// + /// Cannot create a thread within a voice channel. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Cannot create a thread within a voice channel"); + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + #endregion + #region IAudioChannel /// /// Connecting to a REST-based channel is not supported. diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index e0074ecff..f5fce5a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -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> 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> 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> 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> 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 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 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(); + 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.Unspecified, + Flags = flags, + Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + } + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); + + return RestThreadChannel.Create(client, channel.Guild, model); + } + + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + { + embeds ??= Array.Empty(); + 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.Unspecified, + Flags = flags, + MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Slowmode = slowmode, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Title = title + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options); + + return RestThreadChannel.Create(client, channel.Guild, model); + } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 469e93db4..8bab35937 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -132,12 +132,15 @@ namespace Discord.Rest } public static ulong GetUploadLimit(IGuild guild) { - return guild.PremiumTier switch + var tierFactor = guild.PremiumTier switch { - PremiumTier.Tier2 => 50ul * 1000000, - PremiumTier.Tier3 => 100ul * 1000000, - _ => 8ul * 1000000 + PremiumTier.Tier2 => 50, + PremiumTier.Tier3 => 100, + _ => 8 }; + + var mebibyte = Math.Pow(2, 20); + return (ulong) (tierFactor * mebibyte); } #endregion @@ -151,7 +154,7 @@ namespace Discord.Rest if (fromUserId.HasValue) return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); - else + else return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); } @@ -908,7 +911,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); - + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 92d598466..974ea69ad 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1161,7 +1161,6 @@ namespace Discord.Rest /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 196416f0e..22e56a733 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -39,16 +39,16 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestCommandBase(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs index 4227c802a..828299d22 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs @@ -27,20 +27,20 @@ namespace Discord.Rest { } - internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestCommandBaseData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { Name = model.Name; if (model.Resolved.IsSpecified && ResolvableData == null) { ResolvableData = new RestResolvableData(); - await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false); + await ResolvableData.PopulateAsync(client, guild, channel, model, doApiCall).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs index 9353a8530..72b894729 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs @@ -22,7 +22,7 @@ namespace Discord.Rest internal readonly Dictionary Attachments = new Dictionary(); - internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model) + internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model, bool doApiCall) { var resolved = model.Resolved.Value; @@ -38,15 +38,26 @@ namespace Discord.Rest if (resolved.Channels.IsSpecified) { - var channels = await guild.GetChannelsAsync().ConfigureAwait(false); + var channels = doApiCall ? await guild.GetChannelsAsync().ConfigureAwait(false) : null; foreach (var channelModel in resolved.Channels.Value) { - var restChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); + if (channels != null) + { + var guildChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); - restChannel.Update(channelModel.Value); + guildChannel.Update(channelModel.Value); - Channels.Add(ulong.Parse(channelModel.Key), restChannel); + Channels.Add(ulong.Parse(channelModel.Key), guildChannel); + } + else + { + var restChannel = RestChannel.Create(discord, channelModel.Value); + + restChannel.Update(channelModel.Value); + + Channels.Add(ulong.Parse(channelModel.Key), restChannel); + } } } @@ -76,7 +87,10 @@ namespace Discord.Rest { foreach (var msg in resolved.Messages.Value) { - channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value ?? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false)); + channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value + ?? (doApiCall + ? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false) + : null)); RestUser author; diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs index 609fe0829..34c664b09 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs @@ -20,22 +20,22 @@ namespace Discord.Rest } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IMessageCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs index 127d539d9..d2968a38a 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs @@ -23,15 +23,15 @@ namespace Discord.Rest /// Note Not implemented for /// public override IReadOnlyCollection Options - => throw new System.NotImplementedException(); + => throw new NotImplementedException(); internal RestMessageCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestMessageCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs index 7f55fd61b..91319a649 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestUserCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IUserCommandInteractionData diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs index e18499d42..61b291f7c 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs @@ -26,10 +26,10 @@ namespace Discord.Rest internal RestUserCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestUserCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 74d7953ad..522c098e6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -369,7 +369,7 @@ namespace Discord.Rest #endregion #region Responses - public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, + public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, RequestOptions options = null) { var args = new MessageProperties(); @@ -411,7 +411,7 @@ namespace Discord.Rest } public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); - public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, + public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, RequestOptions options = null) { var args = new MessageProperties(); diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs new file mode 100644 index 000000000..03750d7d9 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + /// + /// Represents a class that contains data present in all interactions to evaluate against at rest-interaction creation. + /// + public readonly struct InteractionProperties + { + /// + /// The type of this interaction. + /// + public InteractionType Type { get; } + + /// + /// Gets the type of application command this interaction represents. + /// + /// + /// This will be if the is not . + /// + public ApplicationCommandType? CommandType { get; } + + /// + /// Gets the name of the interaction. + /// + /// + /// This will be if the is not . + /// + public string Name { get; } = string.Empty; + + /// + /// Gets the custom ID of the interaction. + /// + /// + /// This will be if the is not or . + /// + public string CustomId { get; } = string.Empty; + + /// + /// Gets the guild ID of the interaction. + /// + /// + /// This will be if this interaction was not executed in a guild. + /// + public ulong? GuildId { get; } + + /// + /// Gets the channel ID of the interaction. + /// + /// + /// This will be if this interaction is . + /// + public ulong? ChannelId { get; } + + internal InteractionProperties(API.Interaction model) + { + Type = model.Type; + CommandType = null; + + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + else + GuildId = null; + + if (model.ChannelId.IsSpecified) + ChannelId = model.ChannelId.Value; + else + ChannelId = null; + + switch (Type) + { + case InteractionType.ApplicationCommand: + { + var data = (API.ApplicationCommandInteractionData)model.Data; + + CommandType = data.Type; + Name = data.Name; + } + break; + case InteractionType.MessageComponent: + { + var data = (API.MessageComponentInteractionData)model.Data; + + CustomId = data.CustomId; + } + break; + case InteractionType.ModalSubmit: + { + var data = (API.ModalInteractionData)model.Data; + + CustomId = data.CustomId; + } + break; + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 002510eac..e0eab6051 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -37,15 +37,15 @@ namespace Discord.Rest Data = new RestMessageComponentData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageComponent(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient discord, Model model) + internal override async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { - await base.UpdateAsync(discord, model).ConfigureAwait(false); + await base.UpdateAsync(discord, model, doApiCall).ConfigureAwait(false); if (model.Message.IsSpecified && model.ChannelId.IsSpecified) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index 5f54fe051..9229b63b5 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -26,10 +26,10 @@ namespace Discord.Rest Data = new RestModalData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model) + internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) { var entity = new RestModal(client, model); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index 9e2bab2c2..667609ef4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -65,8 +65,7 @@ namespace Discord.Rest : ImmutableArray.Create(); IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); - DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified - ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 8a8921abe..43d13f521 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; using Model = Discord.API.Interaction; @@ -16,6 +15,10 @@ namespace Discord.Rest /// public abstract class RestInteraction : RestEntity, IDiscordInteraction { + // Added so channel & guild methods don't need a client reference + private Func> _getChannel; + private Func> _getGuild; + /// public InteractionType Type { get; private set; } @@ -31,6 +34,10 @@ namespace Discord.Rest /// /// Gets the user who invoked the interaction. /// + /// + /// If this user is an and is set to false, + /// will return + /// public RestUser User { get; private set; } /// @@ -51,19 +58,36 @@ namespace Discord.Rest /// /// Gets the channel that this interaction was executed in. /// + /// + /// This property will be if is set to false. + /// Call to set this property and get the interaction channel. + /// public IRestMessageChannel Channel { get; private set; } + /// + public ulong? ChannelId { get; private set; } + /// - /// Gets the guild this interaction was executed in. + /// Gets the guild this interaction was executed in if applicable. /// + /// + /// This property will be if is set to false + /// or if the interaction was not executed in a guild. + /// public RestGuild Guild { get; private set; } + /// + public ulong? GuildId { get; private set; } + /// public bool HasResponded { get; protected set; } /// public bool IsDMInteraction { get; private set; } + /// + public ulong ApplicationId { get; private set; } + internal RestInteraction(BaseDiscordClient discord, ulong id) : base(discord, id) { @@ -72,11 +96,11 @@ namespace Discord.Rest : DateTime.UtcNow; } - internal static async Task CreateAsync(DiscordRestClient client, Model model) + internal static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { if(model.Type == InteractionType.Ping) { - return await RestPingInteraction.CreateAsync(client, model); + return await RestPingInteraction.CreateAsync(client, model, doApiCall); } if (model.Type == InteractionType.ApplicationCommand) @@ -90,65 +114,97 @@ namespace Discord.Rest return dataModel.Type switch { - ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model).ConfigureAwait(false), + ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), _ => null }; } if (model.Type == InteractionType.MessageComponent) - return await RestMessageComponent.CreateAsync(client, model).ConfigureAwait(false); + return await RestMessageComponent.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ApplicationCommandAutocomplete) - return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false); + return await RestAutocompleteInteraction.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ModalSubmit) - return await RestModal.CreateAsync(client, model).ConfigureAwait(false); + return await RestModal.CreateAsync(client, model, doApiCall).ConfigureAwait(false); return null; } - internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model) + internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { - IsDMInteraction = !model.GuildId.IsSpecified; + ChannelId = model.ChannelId.IsSpecified + ? model.ChannelId.Value + : null; + + GuildId = model.GuildId.IsSpecified + ? model.GuildId.Value + : null; + + IsDMInteraction = GuildId is null; Data = model.Data.IsSpecified ? model.Data.Value : null; + Token = model.Token; Version = model.Version; Type = model.Type; + ApplicationId = model.ApplicationId; - if(Guild == null && model.GuildId.IsSpecified) + if (Guild is null && GuildId is not null) { - Guild = await discord.GetGuildAsync(model.GuildId.Value); + if (doApiCall) + Guild = await discord.GetGuildAsync(GuildId.Value); + else + { + Guild = null; + _getGuild = async (opt, ul) => await discord.GetGuildAsync(ul, opt); + } } - if (User == null) + if (User is null) { - if (model.Member.IsSpecified && model.GuildId.IsSpecified) + if (model.Member.IsSpecified && GuildId is not null) { - User = RestGuildUser.Create(Discord, Guild, model.Member.Value); + User = RestGuildUser.Create(Discord, Guild, model.Member.Value, GuildId); } else { User = RestUser.Create(Discord, model.User.Value); } } + - if(Channel == null && model.ChannelId.IsSpecified) + if (Channel is null && ChannelId is not null) { try { - Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + if (doApiCall) + Channel = (IRestMessageChannel)await discord.GetChannelAsync(ChannelId.Value); + else + { + Channel = null; + + _getChannel = async (opt, ul) => + { + if (Guild is null) + return (IRestMessageChannel)await discord.GetChannelAsync(ul, opt); + + // get a guild channel if the guild is set. + return (IRestMessageChannel)await Guild.GetChannelAsync(ul, opt); + }; + } } - catch(HttpException x) when(x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore + catch (HttpException x) when (x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore } UserLocale = model.UserLocale.IsSpecified - ? model.UserLocale.Value - : null; + ? model.UserLocale.Value + : null; + GuildLocale = model.GuildLocale.IsSpecified ? model.GuildLocale.Value : null; @@ -164,6 +220,54 @@ namespace Discord.Rest return json.ToString(); } + /// + /// Gets the channel this interaction was executed in. Will be a DM channel if the interaction was executed in DM. + /// + /// + /// Calling this method successfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// A Rest channel to send messages to. + /// Thrown if no channel can be received. + public async Task GetChannelAsync(RequestOptions options = null) + { + if (Channel is not null) + return Channel; + + if (IsDMInteraction) + { + Channel = await User.CreateDMChannelAsync(options); + } + else if (ChannelId is not null) + { + Channel = await _getChannel(options, ChannelId.Value) ?? throw new InvalidOperationException("The interaction channel was not able to be retrieved."); + _getChannel = null; // get rid of it, we don't need it anymore. + } + + return Channel; + } + + /// + /// Gets the guild this interaction was executed in if applicable. + /// + /// + /// Calling this method successfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// The guild this interaction was executed in. if the interaction was executed inside DM. + public async Task GetGuildAsync(RequestOptions options) + { + if (GuildId is null) + return null; + + Guild ??= await _getGuild(options, GuildId.Value); + _getGuild = null; // get rid of it, we don't need it anymore. + + return Guild; + } + /// public abstract string Defer(bool ephemeral = false, RequestOptions options = null); /// @@ -333,7 +437,6 @@ namespace Discord.Rest => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); - /// #if NETCOREAPP3_0_OR_GREATER != true /// Task IDiscordInteraction.RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs index bd15bc2d3..47e1a3b0f 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -18,10 +18,10 @@ namespace Discord.Rest { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestPingInteraction(client, model.Id); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs index 24dbae37a..27c536240 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -32,10 +32,10 @@ namespace Discord.Rest Data = new RestAutocompleteInteractionData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestAutocompleteInteraction(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs index 21184fcf6..f955e7855 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestSlashCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //ISlashCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs index f967cc628..19a819ab4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs @@ -14,15 +14,15 @@ namespace Discord.Rest internal RestSlashCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestSlashCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { - await base.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await base.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); Options = model.Options.IsSpecified ? model.Options.Value.Select(x => new RestSlashCommandDataOption(this, x)).ToImmutableArray() diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index c48a60aac..69e038fd2 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -144,7 +144,8 @@ namespace Discord.Rest { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index a2ad4fd77..df629bec7 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -25,7 +25,7 @@ namespace Discord.Rest public string Name { get; private set; } /// public string Icon { get; private set; } - /// /> + /// public Emoji Emoji { get; private set; } /// public GuildPermissions Permissions { get; private set; } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 0a4a33099..6c311b6b5 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -35,7 +35,7 @@ namespace Discord.Rest /// public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); /// - public ulong GuildId => Guild.Id; + public ulong GuildId { get; } /// public bool? IsPending { get; private set; } /// @@ -80,14 +80,16 @@ namespace Discord.Rest /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) + internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong? guildId = null) : base(discord, id) { - Guild = guild; + if (guild is not null) + Guild = guild; + GuildId = guildId ?? Guild.Id; } - internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model) + internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? guildId = null) { - var entity = new RestGuildUser(discord, guild, model.User.Id); + var entity = new RestGuildUser(discord, guild, model.User.Id, guildId); entity.Update(model); return entity; } @@ -116,7 +118,7 @@ namespace Discord.Rest private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(Guild.Id); + roles.Add(GuildId); for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4062cda3d..f5a88486b 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -87,6 +87,7 @@ namespace Discord.Rest ChannelId = entity.InternalChannelId, GuildId = entity.GuildId, MessageId = entity.MessageId, + FailIfNotExists = entity.FailIfNotExists }; } public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) diff --git a/src/Discord.Net.Rest/Extensions/StringExtensions.cs b/src/Discord.Net.Rest/Extensions/StringExtensions.cs new file mode 100644 index 000000000..4981a4298 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/StringExtensions.cs @@ -0,0 +1,47 @@ +using Discord.Net.Converters; +using Newtonsoft.Json; +using System.Linq; +using System; + +namespace Discord.Rest +{ + /// + /// Responsible for formatting certain entities as Json , to reuse later on. + /// + public static class StringExtensions + { + private static Lazy _settings = new(() => + { + var serializer = new JsonSerializerSettings() + { + ContractResolver = new DiscordContractResolver() + }; + serializer.Converters.Add(new EmbedTypeConverter()); + return serializer; + }); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The builder to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this EmbedBuilder builder, Formatting formatting = Formatting.Indented) + => ToJsonString(builder.Build(), formatting); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The embed to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this Embed embed, Formatting formatting = Formatting.Indented) + => JsonConvert.SerializeObject(embed.ToModel(), formatting, _settings.Value); + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs index 27cbe9290..d7655a30a 100644 --- a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Globalization; @@ -14,7 +14,7 @@ namespace Discord.Net.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + return ulong.Parse(reader.Value?.ToString(), NumberStyles.None, CultureInfo.InvariantCulture); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs index cfd64104d..43cd3f902 100644 --- a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs +++ b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs @@ -243,7 +243,7 @@ namespace Discord.Net.ED25519 /// /// // Decode a base58-encoded string into byte array /// - /// Base58 data string + /// Base58 data string /// Byte array public static byte[] Base58Decode(string input) { diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 75e79eec2..4915a5c39 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -60,14 +60,9 @@ namespace Discord.Net.Queue _clearToken?.Cancel(); _clearToken?.Dispose(); _clearToken = new CancellationTokenSource(); - if (_parentToken != null) - { - _requestCancelTokenSource?.Dispose(); - _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); - _requestCancelToken = _requestCancelTokenSource.Token; - } - else - _requestCancelToken = _clearToken.Token; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; } finally { _tokenLock.Release(); } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 2ce89be5b..a4355bc02 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -8,6 +8,8 @@ net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 true + 5 + True diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index aaef4656a..5743d9abd 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -403,7 +403,7 @@ namespace Discord.WebSocket /// the snowflake identifier; null if the user is not found. /// public async ValueTask GetUserAsync(ulong id, RequestOptions options = null) - => await ClientHelper.GetUserAsync(this, id, options).ConfigureAwait(false); + => await ((IDiscordClient)this).GetUserAsync(id, CacheMode.AllowDownload, options).ConfigureAwait(false); /// /// Clears all cached channels from the client. /// @@ -1305,13 +1305,13 @@ namespace Discord.WebSocket user.Update(State, data); - var cacheableBefore = new Cacheable(before, user.Id, true, () => null); + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(null, user.Id, false, () => null); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult(null)); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } @@ -2331,7 +2331,9 @@ namespace Discord.WebSocket SocketUser user = data.User.IsSpecified ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. + : guild != null + ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. + : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); SocketChannel channel = null; if(data.ChannelId.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs new file mode 100644 index 000000000..bc6e28442 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -0,0 +1,128 @@ +using Discord.Rest; +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.WebSocket +{ + /// + /// Represents a forum channel in a guild. + /// + public class SocketForumChannel : SocketGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + + internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } + + internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketForumChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + IsNsfw = model.Nsfw.GetValueOrDefault(false); + Topic = model.Topic.GetValueOrDefault(); + DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + public Task 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); + + /// + public async Task 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); + } + + /// + public async Task 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); + } + + /// + public Task 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); + + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable 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); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task 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 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 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 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 IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable 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 + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 79f02fe1c..16ed7b32d 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -59,6 +59,7 @@ namespace Discord.WebSocket ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.Create(guild, state, model), ChannelType.Stage => SocketStageChannel.Create(guild, state, model), + ChannelType.Forum => SocketForumChannel.Create(guild, state, model), _ => new SocketGuildChannel(guild.Discord, model.Id, guild), }; } @@ -222,6 +223,8 @@ namespace Discord.WebSocket #region IChannel /// + string IChannel.Name => Name; + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 91bca5054..56cd92185 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -15,7 +15,11 @@ namespace Discord.WebSocket public class SocketStageChannel : SocketVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -49,19 +53,16 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } - internal void Update(StageInstance model, bool isLive = false) { IsLive = isLive; if (isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e4a299edc..6aece7d78 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -40,7 +40,8 @@ namespace Discord.WebSocket private bool _nsfw; /// public bool IsNsfw => _nsfw; - + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } /// public string Mention => MentionUtils.MentionChannel(Id); /// @@ -76,6 +77,11 @@ namespace Discord.WebSocket Topic = model.Topic.GetValueOrDefault(); SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet? _nsfw = model.Nsfw.GetValueOrDefault(); + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// @@ -128,7 +134,7 @@ namespace Discord.WebSocket #region Messages /// - public SocketMessage GetCachedMessage(ulong id) + public virtual SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); /// /// Gets a message from this message channel. @@ -143,7 +149,7 @@ namespace Discord.WebSocket /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - public async Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual async Task GetMessageAsync(ulong id, RequestOptions options = null) { IMessage msg = _messages?.Get(id); if (msg == null) @@ -163,7 +169,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -179,7 +185,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -195,25 +201,25 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); /// - public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); /// - public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); /// - public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -221,7 +227,7 @@ namespace Discord.WebSocket /// /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -230,7 +236,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -239,7 +245,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -248,7 +254,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -256,28 +262,28 @@ namespace Discord.WebSocket messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); internal void AddMessage(SocketMessage msg) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 00003d4ed..7bf65d638 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -14,33 +15,25 @@ namespace Discord.WebSocket /// Represents a WebSocket-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel + public class SocketVoiceChannel : SocketTextChannel, IVoiceChannel, ISocketAudioChannel { #region SocketVoiceChannel - /// - public int Bitrate { get; private set; } - /// - public int? UserLimit { get; private set; } - /// - public string RTCRegion { get; private set; } - - /// - public ulong? CategoryId { get; private set; } /// - /// Gets the parent (category) channel of this channel. + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. /// - /// - /// A category channel representing the parent of this channel; null if none is set. - /// - public ICategoryChannel Category - => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// + /// Discord currently doesn't have a way to disable Text-In-Voice yet so this field is always + /// on s and on + /// s. + /// + public virtual bool IsTextInVoice => true; /// - public string Mention => MentionUtils.MentionChannel(Id); - + public int Bitrate { get; private set; } + /// + public int? UserLimit { get; private set; } /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + public string RTCRegion { get; private set; } /// /// Gets a collection of users that are currently connected to this voice channel. @@ -48,7 +41,7 @@ namespace Discord.WebSocket /// /// A read-only collection of users that are currently connected to this voice channel. /// - public override IReadOnlyCollection Users + public IReadOnlyCollection ConnectedUsers => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) @@ -65,7 +58,6 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - CategoryId = model.CategoryId; Bitrate = model.Bitrate.Value; UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; RTCRegion = model.RTCRegion.GetValueOrDefault(null); @@ -99,28 +91,215 @@ namespace Discord.WebSocket return user; return null; } -#endregion - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// Cannot create threads in voice channels. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Voice channels cannot contain threads."); + + /// Cannot modify text channel properties for voice channels. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties for voice channels."); + + #endregion + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override SocketMessage GetCachedMessage(ulong id) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessage(id); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessage, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessageId, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; - #endregion #region IGuildChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 8b376b3ed..9ce2f507a 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -705,7 +705,15 @@ namespace Discord.WebSocket /// public SocketThreadChannel GetThreadChannel(ulong id) => GetChannel(id) as SocketThreadChannel; - + /// + /// Gets a forum channel in this guild. + /// + /// The snowflake identifier for the forum channel. + /// + /// A forum channel associated with the specified ; if none is found. + /// + public SocketForumChannel GetForumChannel(ulong id) + => GetChannel(id) as SocketForumChannel; /// /// Gets a voice channel in this guild. /// @@ -1291,7 +1299,6 @@ namespace Discord.WebSocket /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index aeff465bd..4f9a769c2 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -226,8 +226,12 @@ namespace Discord.WebSocket bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(Message.Content); bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || Message.Embeds.Any(); + bool hasComponents = args.Components.IsSpecified && args.Components.Value != null; + bool hasAttachments = args.Attachments.IsSpecified; + bool hasFlags = args.Flags.IsSpecified; - if (!hasText && !hasEmbeds) + // No content needed if modifying flags + if ((!hasComponents && !hasText && !hasEmbeds && !hasAttachments) && !hasFlags) Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; @@ -261,20 +265,41 @@ namespace Discord.WebSocket } } - var response = new API.InteractionResponse + if (!args.Attachments.IsSpecified) { - Type = InteractionResponseType.UpdateMessage, - Data = new API.InteractionCallbackData + var response = new API.InteractionResponse { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + } + else + { + var response = new API.Rest.UploadInteractionFileParams(args.Attachments.Value.ToArray()) + { + Type = InteractionResponseType.UpdateMessage, Content = args.Content, AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, - Components = args.Components.IsSpecified + MessageComponents = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified - } - }; + }; + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + } lock (_lock) { @@ -284,7 +309,6 @@ namespace Discord.WebSocket } } - await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); HasResponded = true; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index cfbd3096d..647544b48 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -174,6 +174,91 @@ namespace Discord.WebSocket HasResponded = true; } + public async Task UpdateAsync(Action func, RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed."); + } + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : false; + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0; + + if (!hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue) + { + var allowedMentions = args.AllowedMentions.Value; + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) + && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) + && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + /// public override async Task FollowupAsync( string text = null, diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 40ec17f5b..8f27b65f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -94,8 +94,7 @@ namespace Discord.WebSocket : ImmutableArray.Create(); IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); - DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified - ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index d722c5a13..a629fd069 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -59,7 +59,7 @@ namespace Discord.WebSocket } } - if (resolved.Members.IsSpecified) + if (resolved.Members.IsSpecified && guild != null) { foreach (var member in resolved.Members.Value) { diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 5b2da04f5..f8eb6b12e 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -24,20 +24,11 @@ namespace Discord.WebSocket /// public ISocketMessageChannel Channel { get; private set; } - /// - /// Gets the ID of the channel this interaction was used in. - /// - /// - /// This property is exposed in cases where the bot scope is not provided, so the channel entity cannot be retrieved. - ///
- /// To get the channel, you can call - /// as this method makes a request for a if nothing was found in cache. - ///
+ /// public ulong? ChannelId { get; private set; } /// /// Gets the who triggered this interaction. - /// This property will be if the bot scope isn't used. /// public SocketUser User { get; private set; } @@ -74,6 +65,12 @@ namespace Discord.WebSocket /// public bool IsDMInteraction { get; private set; } + /// + public ulong? GuildId { get; private set; } + + /// + public ulong ApplicationId { get; private set; } + internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user) : base(client, id) { @@ -119,13 +116,21 @@ namespace Discord.WebSocket internal virtual void Update(Model model) { - IsDMInteraction = !model.GuildId.IsSpecified; + ChannelId = model.ChannelId.IsSpecified + ? model.ChannelId.Value + : null; - ChannelId = model.ChannelId.ToNullable(); + GuildId = model.GuildId.IsSpecified + ? model.GuildId.Value + : null; + + IsDMInteraction = GuildId is null; + ApplicationId = model.ApplicationId; Data = model.Data.IsSpecified ? model.Data.Value : null; + Token = model.Token; Version = model.Version; Type = model.Type; @@ -133,6 +138,7 @@ namespace Discord.WebSocket UserLocale = model.UserLocale.IsSpecified ? model.UserLocale.Value : null; + GuildLocale = model.GuildLocale.IsSpecified ? model.GuildLocale.Value : null; @@ -392,7 +398,7 @@ namespace Discord.WebSocket /// The request options for this request. /// A task that represents the asynchronous operation of responding to the interaction. public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null); - #endregion +#endregion /// /// Attepts to get the channel this interaction was executed in. @@ -416,7 +422,7 @@ namespace Discord.WebSocket catch(HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) { return null; } // bot can't view that channel, return null instead of throwing. } - #region IDiscordInteraction +#region IDiscordInteraction /// IUser IDiscordInteraction.User => User; @@ -446,6 +452,6 @@ namespace Discord.WebSocket async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); #endif - #endregion +#endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 6668426e1..3cd67beb5 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -182,7 +182,8 @@ namespace Discord.WebSocket { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index df920b7dc..1e3c3f7f8 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -6,6 +6,8 @@ Discord.Webhook A core Discord.Net library containing the Webhook client and models. net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 405100f89..556338956 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -88,8 +88,8 @@ namespace Discord.Webhook /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags, threadId); /// /// Modifies a message posted using this webhook. @@ -103,8 +103,8 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous modification operation. /// - public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) - => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null, ulong? threadId = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options, threadId); /// /// Deletes a message posted using this webhook. @@ -117,43 +117,43 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous deletion operation. /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => WebhookClientHelper.DeleteMessageAsync(this, messageId, options); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null, ulong ? threadId = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, options, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components, flags); + allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components, flags); + avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options, flags); + avatarUrl, allowedMentions, components, options, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options, flags); + allowedMentions, components, options, flags, threadId); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 0a974a9d9..8ad74e7e7 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,8 +21,8 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, - AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags, ulong? threadId = null) { var args = new CreateWebhookMessageParams { @@ -44,12 +44,13 @@ namespace Discord.Webhook if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); - - var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options, threadId: threadId).ConfigureAwait(false); return model.Id; } + public static async Task ModifyMessageAsync(DiscordWebhookClient client, ulong messageId, - Action func, RequestOptions options) + Action func, RequestOptions options, ulong? threadId) { var args = new WebhookMessageProperties(); func(args); @@ -94,35 +95,35 @@ namespace Discord.Webhook Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, }; - await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options) + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) .ConfigureAwait(false); } - public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options) + public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options, ulong? threadId) { - await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); + await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options, threadId).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, - bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None, ulong? threadId = null) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId).ConfigureAwait(false); } public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, - MessageComponent components, MessageFlags flags) - => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, MessageFlags flags, ulong? threadId) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, - MessageComponent components, RequestOptions options, MessageFlags flags) - => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, RequestOptions options, MessageFlags flags, ulong? threadId) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static async Task SendFilesAsync(DiscordWebhookClient client, IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, - MessageFlags flags) + MessageFlags flags, ulong? threadId) { embeds ??= Array.Empty(); @@ -164,7 +165,7 @@ namespace Discord.Webhook MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Flags = flags }; - var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options, threadId).ConfigureAwait(false); return msg.Id; } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index c41f844e1..1a61ff97a 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,57 +2,57 @@ Discord.Net - 3.6.0$suffix$ + 3.7.2$suffix$ Discord.Net Discord.Net Contributors foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp - https://github.com/RogueException/Discord.Net + https://github.com/discord-net/Discord.Net http://opensource.org/licenses/MIT false - https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - \ No newline at end of file + diff --git a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj index 0f399ab68..7b8257bfb 100644 --- a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj +++ b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs new file mode 100644 index 000000000..96b33b141 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs @@ -0,0 +1,53 @@ +using Discord.API; +using Discord.API.Rest; +using Discord.Net; +using Discord.Rest; +using FluentAssertions; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Discord; + +[CollectionDefinition(nameof(DiscordRestApiClientTests), DisableParallelization = true)] +public class DiscordRestApiClientTests : IClassFixture, IAsyncDisposable +{ + private readonly DiscordRestApiClient _apiClient; + private readonly IGuild _guild; + private readonly ITextChannel _channel; + + public DiscordRestApiClientTests(RestGuildFixture guildFixture) + { + _guild = guildFixture.Guild; + _apiClient = guildFixture.Client.ApiClient; + _channel = _guild.CreateTextChannelAsync("testChannel").Result; + } + + public async ValueTask DisposeAsync() + { + await _channel.DeleteAsync(); + } + + [Fact] + public async Task UploadFile_WithMaximumSize_DontThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild); + using var stream = new MemoryStream(new byte[fileSize]); + + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + } + + [Fact] + public async Task UploadFile_WithOverSize_ThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild) + 1; + using var stream = new MemoryStream(new byte[fileSize]); + + Func upload = async () => + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + + await upload.Should().ThrowExactlyAsync() + .Where(e => e.DiscordCode == DiscordErrorCode.RequestEntityTooLarge); + } +} diff --git a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj index ec06c3c3d..087a64d83 100644 --- a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj +++ b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj @@ -12,7 +12,9 @@ + + all diff --git a/test/Discord.Net.Tests.Unit/GuildHelperTests.cs b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs new file mode 100644 index 000000000..c68f415fe --- /dev/null +++ b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs @@ -0,0 +1,25 @@ +using Discord.Rest; +using FluentAssertions; +using Moq; +using System; +using Xunit; + +namespace Discord; + +public class GuildHelperTests +{ + [Theory] + [InlineData(PremiumTier.None, 8)] + [InlineData(PremiumTier.Tier1, 8)] + [InlineData(PremiumTier.Tier2, 50)] + [InlineData(PremiumTier.Tier3, 100)] + public void GetUploadLimit(PremiumTier tier, ulong factor) + { + var guild = Mock.Of(g => g.PremiumTier == tier); + var expected = factor * (ulong)Math.Pow(2, 20); + + var actual = GuildHelper.GetUploadLimit(guild); + + actual.Should().Be(expected); + } +} diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 0dfcab7a5..ab1d3e534 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -10,6 +10,8 @@ namespace Discord { public bool IsNsfw => throw new NotImplementedException(); + public ThreadArchiveDuration DefaultArchiveDuration => throw new NotImplementedException(); + public string Topic => throw new NotImplementedException(); public int SlowModeInterval => throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 533b1b1b5..fdbdeda5e 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; using Discord.Audio; @@ -12,8 +13,6 @@ namespace Discord public int? UserLimit => throw new NotImplementedException(); - public string Mention => throw new NotImplementedException(); - public ulong? CategoryId => throw new NotImplementedException(); public int Position => throw new NotImplementedException(); @@ -24,116 +23,53 @@ namespace Discord public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + public string RTCRegion => throw new NotImplementedException(); + public string Name => throw new NotImplementedException(); public DateTimeOffset CreatedAt => throw new NotImplementedException(); - public ulong Id => throw new NotImplementedException(); - - public string RTCRegion => throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } + public ulong Id => throw new NotImplementedException(); - public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) - { - throw new NotImplementedException(); - } + public string Mention => throw new NotImplementedException(); - public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - { - throw new NotImplementedException(); - } - public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException(); + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); - public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - - public Task DeleteAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task DisconnectAsync() - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options) - { - throw new NotImplementedException(); - } - - public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task> GetInvitesAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - throw new NotImplementedException(); - } - - public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task SyncPermissionsAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } - - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } + public Task CreateInviteToStreamAsync(IUser user, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + public Task DisconnectAsync() => throw new NotImplementedException(); + public IDisposable EnterTypingState(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetInvitesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task TriggerTypingAsync(RequestOptions options = null) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotImplementedException(); } }