diff --git a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj index 5e1548d458..3f3fe3e3dd 100644 --- a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj +++ b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj @@ -28,6 +28,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,6 +37,7 @@ + diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index 5955d8e5ec..643b9876e4 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -380,7 +380,7 @@ public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to var helpMessage = helpBuilder.Build(); - var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed); + var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).AddEmbed(helpMessage.Embed); if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null) await ctx.RespondAsync(builder).ConfigureAwait(false); @@ -531,7 +531,7 @@ public CommandContext CreateContext(DiscordMessage msg, string prefix, Command c ChannelId = msg.ChannelId }; - if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) + if (cmd != null && cmd.Module is TransientCommandModule or null) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new(ctx.Services, scope); diff --git a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj index 8a344139c5..f1b603fcdd 100644 --- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj +++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj @@ -34,6 +34,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -45,6 +46,7 @@ + diff --git a/DisCatSharp.Common/DisCatSharp.Common.csproj b/DisCatSharp.Common/DisCatSharp.Common.csproj index 4a2ba816c8..2a4811024f 100644 --- a/DisCatSharp.Common/DisCatSharp.Common.csproj +++ b/DisCatSharp.Common/DisCatSharp.Common.csproj @@ -26,7 +26,7 @@ - + diff --git a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj index 1622ec7c60..eb1ec0ccbd 100644 --- a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj +++ b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj @@ -23,6 +23,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,6 +33,7 @@ + diff --git a/DisCatSharp.Experimental/DisCatSharp.Experimental.csproj b/DisCatSharp.Experimental/DisCatSharp.Experimental.csproj index 57369fdd55..57b8effc8c 100644 --- a/DisCatSharp.Experimental/DisCatSharp.Experimental.csproj +++ b/DisCatSharp.Experimental/DisCatSharp.Experimental.csproj @@ -26,6 +26,7 @@ + @@ -34,6 +35,7 @@ + diff --git a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj index 9751af62ff..f646484d8d 100644 --- a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj +++ b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj @@ -17,6 +17,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,6 +25,7 @@ + diff --git a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj index 2d9090aaa0..32888a36d4 100644 --- a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj +++ b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj @@ -22,6 +22,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -34,6 +35,7 @@ + diff --git a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj index 10f4fe6ab8..dae9ca1345 100644 --- a/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj +++ b/DisCatSharp.Interactivity/DisCatSharp.Interactivity.csproj @@ -28,6 +28,7 @@ + all @@ -36,6 +37,7 @@ + diff --git a/DisCatSharp.Interactivity/EventHandling/Paginator.cs b/DisCatSharp.Interactivity/EventHandling/Paginator.cs index 5600978159..9907455897 100644 --- a/DisCatSharp.Interactivity/EventHandling/Paginator.cs +++ b/DisCatSharp.Interactivity/EventHandling/Paginator.cs @@ -249,7 +249,7 @@ private async Task PaginateAsync(IPaginationRequest p, DiscordEmoji emoji) var page = await p.GetPageAsync().ConfigureAwait(false); var builder = new DiscordMessageBuilder() .WithContent(page.Content) - .WithEmbed(page.Embed); + .AddEmbed(page.Embed); await builder.ModifyAsync(msg).ConfigureAwait(false); } diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs index 7c6c68da67..436c15eb7f 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -746,7 +746,7 @@ public async Task SendPaginatedMessageAsync( var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) - .WithEmbed(pages.First().Embed) + .AddEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); @@ -830,7 +830,7 @@ public async Task SendPaginatedMessageAsync( { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) - .WithEmbed(pages.First().Embed); + .AddEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutOverride ?? this.Config.Timeout; diff --git a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj index 1feb517eb8..dd8eedd822 100644 --- a/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj +++ b/DisCatSharp.Lavalink/DisCatSharp.Lavalink.csproj @@ -36,6 +36,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -47,6 +48,7 @@ + diff --git a/DisCatSharp.Tools/DisCatSharp.Analyzer/DisCatSharp.Analyzer/DisCatSharp.Analyzer.csproj b/DisCatSharp.Tools/DisCatSharp.Analyzer/DisCatSharp.Analyzer/DisCatSharp.Analyzer.csproj index caeba16893..94f4ee9c48 100644 --- a/DisCatSharp.Tools/DisCatSharp.Analyzer/DisCatSharp.Analyzer/DisCatSharp.Analyzer.csproj +++ b/DisCatSharp.Tools/DisCatSharp.Analyzer/DisCatSharp.Analyzer/DisCatSharp.Analyzer.csproj @@ -37,12 +37,14 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj index 4d3d8bc5c9..29b99f6eae 100644 --- a/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj +++ b/DisCatSharp.VoiceNext/DisCatSharp.VoiceNext.csproj @@ -29,6 +29,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,6 +37,7 @@ + diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj index 162fad0b7a..19456d12e0 100644 --- a/DisCatSharp/DisCatSharp.csproj +++ b/DisCatSharp/DisCatSharp.csproj @@ -26,7 +26,7 @@ - + @@ -56,12 +56,6 @@ - - - - - - diff --git a/DisCatSharp/Entities/Core/DisCatSharpBuilder.cs b/DisCatSharp/Entities/Core/DisCatSharpBuilder.cs new file mode 100644 index 0000000000..7dabd46d16 --- /dev/null +++ b/DisCatSharp/Entities/Core/DisCatSharpBuilder.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; + +namespace DisCatSharp.Entities.Core; + +/// +/// Represents the common base for most builders. +/// +public class DisCatSharpBuilder +{ + /// + /// The attachments of this builder. + /// + internal List AttachmentsInternal { get; } = []; + + /// + /// The components of this builder. + /// + internal List ComponentsInternal { get; } = []; + + /// + /// The embeds of this builder. + /// + internal List EmbedsInternal { get; } = []; + + /// + /// The files of this builder. + /// + internal List FilesInternal { get; } = []; + + /// + /// The allowed mentions of this builder. + /// + internal List MentionsInternal { get; } = []; + + /// + /// The content of this builder. + /// + internal string? ContentInternal { get; set; } + + /// + /// Whether flags were changed in this builder. + /// + internal bool FlagsChanged { get; set; } = false; + + /// + /// Sets the content of this builder. + /// + public string? Content + { + get => this.ContentInternal; + set + { + if (this.IsComponentsV2 || this.IsVoiceMessage) + throw new InvalidOperationException("You cannot set the content for UI Kit / Voice messages"); + + if (value is { Length: > 2000 }) + throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); + + this.ContentInternal = value; + } + } + + /// + /// Gets the components of this builder. + /// + public IReadOnlyList Components + => this.ComponentsInternal; + + /// + /// Gets the embeds of this builder. + /// + public IReadOnlyList Embeds + => this.EmbedsInternal; + + /// + /// Gets the attachments of this builder. + /// + public IReadOnlyList Attachments + => this.AttachmentsInternal; + + /// + /// Gets the files of this builder. + /// + public IReadOnlyList Files + => this.FilesInternal; + + /// + /// Gets the allowed mentions of this builder. + /// + public IReadOnlyList Mentions + => this.MentionsInternal; + + /// + /// Sets whether this builder sends a voice message. + /// You can't use that on your own, it needs DisCatSharp.Experimental. + /// + internal bool IsVoiceMessage + { + get => this.VOICE_MSG; + set + { + this.VOICE_MSG = value; + this.FlagsChanged = true; + } + } + + /// + /// Whether this builder sends a voice message. + /// + private bool VOICE_MSG { get; set; } + + /// + /// Sets whether this builder should send a silent message. + /// + public bool NotificationsSuppressed + { + get => this.NOTIFICATIONS_SUPPRESSED; + set + { + this.NOTIFICATIONS_SUPPRESSED = value; + this.FlagsChanged = true; + } + } + + /// + /// Whether this builder sends a silent message. + /// + private bool NOTIFICATIONS_SUPPRESSED { get; set; } + + /// + /// Sets whether this builder should be using UI Kit. + /// + public bool IsComponentsV2 + { + get => this.IS_COMPONENTS_V2; + set + { + this.IS_COMPONENTS_V2 = value; + this.FlagsChanged = true; + } + } + + /// + /// Whether this builder is using UI Kit. + /// + private bool IS_COMPONENTS_V2 { get; set; } + + /// + /// Sets whether this builder suppresses its embeds. + /// + public bool EmbedsSuppressed + { + get => this.EMBEDS_SUPPRESSED; + set + { + if (this.IsComponentsV2) + throw new InvalidOperationException("You cannot set embeds suppressed for UI Kit messages since they cannot have embeds"); + + this.EMBEDS_SUPPRESSED = value; + this.FlagsChanged = true; + } + } + + /// + /// Whether this builder has its embeds suppressed. + /// + private bool EMBEDS_SUPPRESSED { get; set; } + + /// + /// Clears the components on this builder. + /// + public void ClearComponents() + => this.ComponentsInternal.Clear(); + + /// + /// Allows for clearing the builder so that it can be used again. + /// + public virtual void Clear() + { + this.Content = null; + this.FilesInternal.Clear(); + this.EmbedsInternal.Clear(); + this.AttachmentsInternal.Clear(); + this.ComponentsInternal.Clear(); + this.IsVoiceMessage = false; + this.IsComponentsV2 = false; + this.EmbedsSuppressed = false; + this.NotificationsSuppressed = false; + this.FlagsChanged = false; + this.MentionsInternal.Clear(); + } + + /// + /// Validates the builder. + /// + internal virtual void Validate() + { } +} diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordComponent.cs b/DisCatSharp/Entities/Interaction/Components/DiscordComponent.cs index ee2508d5dd..7d011b9c5b 100644 --- a/DisCatSharp/Entities/Interaction/Components/DiscordComponent.cs +++ b/DisCatSharp/Entities/Interaction/Components/DiscordComponent.cs @@ -24,8 +24,14 @@ internal DiscordComponent() public ComponentType Type { get; internal set; } /// - /// The Id of this component, if applicable. Not applicable on ActionRow(s) and link buttons. + /// The custom Id of this component, if applicable. Not applicable on ActionRow(s) and link buttons. /// [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] - public string CustomId { get; internal set; } + public string? CustomId { get; internal set; } + + /// + /// Gets the Id of the compenent. Determined by Discord. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public uint Id { get; internal set; } } diff --git a/DisCatSharp/Entities/Interaction/Components/DiscordEmojiComponent.cs b/DisCatSharp/Entities/Interaction/Components/DiscordComponentEmoji.cs similarity index 100% rename from DisCatSharp/Entities/Interaction/Components/DiscordEmojiComponent.cs rename to DisCatSharp/Entities/Interaction/Components/DiscordComponentEmoji.cs diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordContainerComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordContainerComponent.cs new file mode 100644 index 0000000000..c4d4ea6a93 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordContainerComponent.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a container component. +/// +public sealed class DiscordContainerComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordContainerComponent() + { + this.Type = ComponentType.Container; + } + + /// + /// Constructs a new container component based on another container component. + /// + /// The container component to copy. + public DiscordContainerComponent(DiscordContainerComponent other) + : this() + { + this.Components = other.Components; + this.AccentColor = other.AccentColor; + this.Spoiler = other.Spoiler; + } + + /// + /// Constructs a new container component field with the specified options. + /// + /// The container components. Max of 10. + /// Whether the container should be marked as spoiler. + /// The accent color for the container. + public DiscordContainerComponent(IEnumerable components, bool? spoiler = null, DiscordColor? accentColor = null) + : this() + { + var comps = components.ToList(); + if (comps.Count > 10) + throw new ArgumentException("You can only have up to 10 components in a container."); + + List allowedTypes = [ComponentType.ActionRow, ComponentType.TextDisplay, ComponentType.Section, ComponentType.MediaGallery, ComponentType.Separator, ComponentType.File]; + if (comps.Any(c => !allowedTypes.Contains(c.Type))) + throw new ArgumentException("All components must be of type ActionRow, TextDisplay, Section, MediaGallery, Separator, or File."); + + this.Components = [.. comps]; + this.Spoiler = spoiler; + this.AccentColor = accentColor; + } + + /// + /// The components for the container. + /// + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Components { get; internal set; } = []; + + /// + /// The accent color for the container. + /// + [JsonIgnore] + public DiscordColor? AccentColor { get; internal set; } + + /// + /// Gets the accent color int for the container. + /// + [JsonProperty("accent_color", NullValueHandling = NullValueHandling.Ignore)] + internal int? AccentColorInt + => this.AccentColor?.Value; + + /// + /// Gets whether this container should be marked as spoiler. + /// + [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] + public bool? Spoiler { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordFileDisplayComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordFileDisplayComponent.cs new file mode 100644 index 0000000000..0ccf45d9b9 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordFileDisplayComponent.cs @@ -0,0 +1,54 @@ +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a file display component. +/// +public sealed class DiscordFileDisplayComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordFileDisplayComponent() + { + this.Type = ComponentType.File; + } + + /// + /// Constructs a new file display component based on another file display component. + /// + /// The file display component to copy. + public DiscordFileDisplayComponent(DiscordFileDisplayComponent other) + : this() + { + this.File = other.File; + this.Spoiler = other.Spoiler; + } + + /// + /// Constructs a new file display component field with the specified options. + /// + /// The file url. + /// Whether this file should be marked as spoiler. + public DiscordFileDisplayComponent(string url, bool? spoiler) + : this() + { + this.File = new(url); + this.Spoiler = spoiler; + } + + /// + /// Gets the file. + /// + [JsonProperty("media")] + public DiscordUnfurledMediaItem File { get; internal set; } + + /// + /// Gets whether this file should be marked as spoiler. + /// + [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] + public bool? Spoiler { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryComponent.cs new file mode 100644 index 0000000000..04d6d74e16 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryComponent.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a media gallery component. +/// +public sealed class DiscordMediaGalleryComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordMediaGalleryComponent() + { + this.Type = ComponentType.MediaGallery; + } + + /// + /// Constructs a new media gallery component based on another media gallery component. + /// + /// The media gallery component to copy. + public DiscordMediaGalleryComponent(DiscordMediaGalleryComponent other) + : this() + { + this.Items = other.Items; + } + + /// + /// Constructs a new media gallery component field with the specified options. + /// + /// The media gallery items. + public DiscordMediaGalleryComponent(IEnumerable items) + : this() + { + var it = items.ToList(); + if (it.Count > 10) + throw new ArgumentException("You can only have up to 10 items in a media gallery."); + + this.Items = it.ToList(); + } + + /// + /// The content for the media gallery. + /// + [JsonProperty("items", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Items { get; internal set; } = []; +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryItem.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryItem.cs new file mode 100644 index 0000000000..c11ec2c823 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordMediaGalleryItem.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a media gallery item. +/// +public sealed class DiscordMediaGalleryItem +{ + /// + /// Constructs a new empty . + /// + internal DiscordMediaGalleryItem() + { } + + /// + /// Constructs a new . + /// + /// The url. + /// The description. + /// Whether this item should be marked as spoiler. + public DiscordMediaGalleryItem(string url, string? description = null, bool? spoiler = null) + { + this.Media = new(url); + this.Description = description; + this.Spoiler = spoiler; + } + + /// + /// Gets the media item. + /// + [JsonProperty("media")] + public DiscordUnfurledMediaItem Media { get; internal set; } + + /// + /// Gets the description. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string? Description { get; internal set; } + + /// + /// Gets whether this gallery item should be marked as spoiler. + /// + [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] + public bool? Spoiler { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionAccessory.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionAccessory.cs new file mode 100644 index 0000000000..848177b235 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionAccessory.cs @@ -0,0 +1,3 @@ +namespace DisCatSharp.Entities; + +public class DiscordSectionAccessory : DiscordComponent; diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionComponent.cs new file mode 100644 index 0000000000..a9ac01ece8 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSectionComponent.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a section component. +/// +public sealed class DiscordSectionComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordSectionComponent() + { + this.Type = ComponentType.Section; + } + + /// + /// Constructs a new section component based on another section component. + /// + /// The section component to copy. + public DiscordSectionComponent(DiscordSectionComponent other) + : this() + { + this.Components = other.Components; + this.Accessory = other.Accessory; + } + + /// + /// Constructs a new section component field with the specified options. + /// + /// The section components. Max of 3. + public DiscordSectionComponent(IEnumerable components) + : this() + { + var comps = components.ToList(); + if (comps.Count > 3) + throw new ArgumentException("You can only have up to 3 components in a section."); + + this.Components = comps.ToList(); + } + + /// + /// The components for the section. + /// + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Components { get; internal set; } = []; + + /// + /// The accessory for the section. + /// Can be at the moment, but might include buttons later. + /// + [JsonProperty("accessory", NullValueHandling = NullValueHandling.Ignore)] + public DiscordSectionAccessory? Accessory { get; internal set; } + + /// + /// Adds a thumbnail component to the section. + /// + /// The thumbnail url. + /// The description of the thumbnail. + /// Whether this thumbnail should be marked as spoiler. + /// The current . + public DiscordSectionComponent WithThumbnailComponent(string url, string? description = null, bool? spoiler = null) + { + this.Accessory = new DiscordThumnailComponent(url, description, spoiler); + return this; + } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSeparatorComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSeparatorComponent.cs new file mode 100644 index 0000000000..0630e0d5f1 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordSeparatorComponent.cs @@ -0,0 +1,57 @@ +using System; + +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a separator component. +/// +public sealed class DiscordSeparatorComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordSeparatorComponent() + { + this.Type = ComponentType.Separator; + } + + /// + /// Constructs a new separator component based on another separator component. + /// + /// The button to copy. + public DiscordSeparatorComponent(DiscordSeparatorComponent other) + : this() + { + this.Divider = other.Divider; + this.Spacing = other.Spacing; + } + + /// + /// Constructs a new separator component field with the specified options. + /// + /// Whether this is a divider. + /// The spacing size. + /// Is thrown when no label is set. + public DiscordSeparatorComponent(bool? divider = null, SeparatorSpacingSize? spacing = null) + : this() + { + this.Divider = divider; + this.Spacing = spacing; + } + + /// + /// The spacing size. + /// + [JsonProperty("spacing", NullValueHandling = NullValueHandling.Ignore)] + public SeparatorSpacingSize? Spacing { get; set; } + + /// + /// Whether this is a divider. + /// + [JsonProperty("divider", NullValueHandling = NullValueHandling.Ignore)] + public bool? Divider { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordTextDisplayComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordTextDisplayComponent.cs new file mode 100644 index 0000000000..9d97dd0992 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordTextDisplayComponent.cs @@ -0,0 +1,45 @@ +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a text display component. +/// +public sealed class DiscordTextDisplayComponent : DiscordComponent +{ + /// + /// Constructs a new . + /// + internal DiscordTextDisplayComponent() + { + this.Type = ComponentType.TextDisplay; + } + + /// + /// Constructs a new text display component based on another text display component. + /// + /// The text display component to copy. + public DiscordTextDisplayComponent(DiscordTextDisplayComponent other) + : this() + { + this.Content = other.Content; + } + + /// + /// Constructs a new text display component field with the specified options. + /// + /// The content for the text display. + public DiscordTextDisplayComponent(string content) + : this() + { + this.Content = content; + } + + /// + /// The content for the text display. + /// + [JsonProperty("content")] + public string Content { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordThumnailComponent.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordThumnailComponent.cs new file mode 100644 index 0000000000..11b4aa1408 --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordThumnailComponent.cs @@ -0,0 +1,51 @@ +using DisCatSharp.Enums; + +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a thumbnail component. +/// +public sealed class DiscordThumnailComponent : DiscordSectionAccessory +{ + /// + /// Constructs a new empty . + /// + internal DiscordThumnailComponent() + { + this.Type = ComponentType.Thumbnail; + } + + /// + /// Constructs a new . + /// + /// The thumbnail url. + /// The description of the thumbnail. + /// Whether this thumbnail should be marked as spoiler. + internal DiscordThumnailComponent(string url, string? description = null, bool? spoiler = null) + : this() + { + this.Media = new(url); + this.Description = description; + this.Spoiler = spoiler; + } + + /// + /// Gets the media item. + /// + [JsonProperty("media")] + public DiscordUnfurledMediaItem Media { get; internal set; } + + /// + /// Gets the description. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string? Description { get; internal set; } + + /// + /// Gets whether this gallery item should be marked as spoiler. + /// + [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] + public bool? Spoiler { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordUnfurledMediaItem.cs b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordUnfurledMediaItem.cs new file mode 100644 index 0000000000..920ebc7c5a --- /dev/null +++ b/DisCatSharp/Entities/Interaction/Components/V2Components/DiscordUnfurledMediaItem.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace DisCatSharp.Entities; + +/// +/// Represents a media item. +/// +public sealed class DiscordUnfurledMediaItem +{ + /// + /// Constructs a new empty . + /// + internal DiscordUnfurledMediaItem() + { } + + /// + /// Constructs a new . + /// + /// The items url. + internal DiscordUnfurledMediaItem(string url) + { + this.Url = url; + } + + /// + /// Gets the url. + /// + [JsonProperty("url")] + public string Url { get; internal set; } +} diff --git a/DisCatSharp/Entities/Interaction/DiscordCallbackHintBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordCallbackHintBuilder.cs index 462ff8a43e..c299bdb7c1 100644 --- a/DisCatSharp/Entities/Interaction/DiscordCallbackHintBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordCallbackHintBuilder.cs @@ -7,12 +7,12 @@ namespace DisCatSharp.Entities; /// /// Represents a callback hint builder for an interaction. /// -public sealed class DiscordCallbackHintBuilder +internal sealed class DiscordCallbackHintBuilder { /// /// Constructs a new . /// - public DiscordCallbackHintBuilder() + internal DiscordCallbackHintBuilder() { this.Clear(); } @@ -21,7 +21,7 @@ public DiscordCallbackHintBuilder() /// Constructs a new from an existing one. /// /// The existing . - public DiscordCallbackHintBuilder(DiscordCallbackHintBuilder other) + internal DiscordCallbackHintBuilder(DiscordCallbackHintBuilder other) { this.Clear(); this.CallbackHints.AddRange(other.CallbackHints); @@ -30,7 +30,7 @@ public DiscordCallbackHintBuilder(DiscordCallbackHintBuilder other) /// /// Gets the callback hints. /// - public List CallbackHints { get; internal set; } = []; + internal List CallbackHints { get; set; } = []; /// /// Adds a callback hint to the builder. @@ -39,7 +39,7 @@ public DiscordCallbackHintBuilder(DiscordCallbackHintBuilder other) /// The intended use of ephemeral. Required if it's only ephemeral. /// The intended required permissions. /// The updated . - public DiscordCallbackHintBuilder AddCallbackHint(InteractionResponseType intendedCallbackType, InteractionCallbackEphemerality intendedCallbackEphemerality = InteractionCallbackEphemerality.Optional, Permissions? intendedRequiredPermissions = null) + internal DiscordCallbackHintBuilder AddCallbackHint(InteractionResponseType intendedCallbackType, InteractionCallbackEphemerality intendedCallbackEphemerality = InteractionCallbackEphemerality.Optional, Permissions? intendedRequiredPermissions = null) { this.CallbackHints.Add(new() { @@ -53,6 +53,6 @@ public DiscordCallbackHintBuilder AddCallbackHint(InteractionResponseType intend /// /// Clears the callback hints. /// - public void Clear() + internal void Clear() => this.CallbackHints.Clear(); } diff --git a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs index f8a164e7fe..5542120fa2 100644 --- a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs @@ -3,28 +3,15 @@ using System.IO; using System.Linq; +using DisCatSharp.Entities.Core; + namespace DisCatSharp.Entities; /// /// Constructs a followup message to an interaction. /// -public sealed class DiscordFollowupMessageBuilder +public sealed class DiscordFollowupMessageBuilder : DisCatSharpBuilder { - private readonly List _components = []; - - private readonly List _embeds = []; - - private readonly List _files = []; - - internal readonly List AttachmentsInternal = []; - - private string _content; - - /// - /// Whether flags were changed. - /// - internal bool FlagsChanged = false; - /// /// Whether this followup message is text-to-speech. /// @@ -46,96 +33,20 @@ public bool IsEphemeral private bool EPH { get; set; } /// - /// Whether to suppress embeds. - /// - public bool EmbedsSuppressed - { - get => this.EMB_SUP; - set - { - this.EMB_SUP = value; - this.FlagsChanged = true; - } - } - - private bool EMB_SUP { get; set; } - - /// - /// Whether to send as voice message. - /// You can't use that on your own, it needs DisCatSharp.Experimental. - /// - internal bool IsVoiceMessage - { - get => this.VOICE_MSG; - set - { - this.VOICE_MSG = value; - this.FlagsChanged = true; - } - } - - private bool VOICE_MSG { get; set; } - - /// - /// Whether to send as silent message. + /// Gets the poll for this message. /// - public bool NotificationsSuppressed - { - get => this.NOTI_SUP; - set - { - this.NOTI_SUP = value; - this.FlagsChanged = true; - } - } - - private bool NOTI_SUP { get; set; } + public DiscordPollBuilder? Poll { get; private set; } /// - /// Message to send on followup message. + /// Sets that this builder should be using UI Kit. /// - public string Content + /// The current builder to chain calls with. + public DiscordFollowupMessageBuilder WithV2Components() { - get => this._content; - set - { - if (value is { Length: > 2000 }) - throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); - - this._content = value; - } + this.IsComponentsV2 = true; + return this; } - /// - /// Embeds to send on followup message. - /// - public IReadOnlyList Embeds => this._embeds; - - /// - /// Files to send on this followup message. - /// - public IReadOnlyList Files => this._files; - - /// - /// Components to send on this followup message. - /// - public IReadOnlyList Components => this._components; - - /// - /// Attachments to be send with this followup request. - /// - public IReadOnlyList Attachments => this.AttachmentsInternal; - - /// - /// Mentions to send on this followup message. - /// - public List? Mentions { get; private set; } - - /// - /// Gets the poll for this message. - /// - public DiscordPollBuilder? Poll { get; private set; } - /// /// Appends a collection of components to the message. /// @@ -154,11 +65,11 @@ public DiscordFollowupMessageBuilder AddComponents(IEnumerable 5) + if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) - this._components.Add(ar); + this.ComponentsInternal.Add(ar); return this; } @@ -171,14 +82,35 @@ public DiscordFollowupMessageBuilder AddComponents(IEnumerable contained more than 5 components. public DiscordFollowupMessageBuilder AddComponents(IEnumerable components) { - var compArr = components.ToArray(); - var count = compArr.Length; + var cmpArr = components.ToArray(); + var count = cmpArr.Length; - if (count > 5) - throw new ArgumentException("Cannot add more than 5 components per action row!"); + if (this.IsComponentsV2) + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 10: + throw new ArgumentException("Cannot add more than 10 components!"); + } + + this.ComponentsInternal.AddRange(cmpArr); + } + else + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 5: + throw new ArgumentException("Cannot add more than 5 components per action row!"); + } + + var comp = new DiscordActionRowComponent(cmpArr); + this.ComponentsInternal.Add(comp); + } - var arc = new DiscordActionRowComponent(compArr); - this._components.Add(arc); return this; } @@ -222,7 +154,8 @@ public DiscordFollowupMessageBuilder WithContent(string content) /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbed(DiscordEmbed embed) { - this._embeds.Add(embed); + ArgumentNullException.ThrowIfNull(embed, nameof(embed)); + this.EmbedsInternal.Add(embed); return this; } @@ -233,7 +166,7 @@ public DiscordFollowupMessageBuilder AddEmbed(DiscordEmbed embed) /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbeds(IEnumerable embeds) { - this._embeds.AddRange(embeds); + this.EmbedsInternal.AddRange(embeds); return this; } @@ -250,16 +183,16 @@ public DiscordFollowupMessageBuilder AddEmbeds(IEnumerable embeds) /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count >= 10) + if (this.FilesInternal.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == filename)) + if (this.FilesInternal.Any(x => x.Filename == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(filename, data, data.Position, description: description)); + this.FilesInternal.Add(new(filename, data, data.Position, description: description)); else - this._files.Add(new(filename, data, null, description: description)); + this.FilesInternal.Add(new(filename, data, null, description: description)); return this; } @@ -276,16 +209,16 @@ public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count >= 10) + if (this.FilesInternal.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == stream.Name)) + if (this.FilesInternal.Any(x => x.Filename == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(stream.Name, stream, stream.Position, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new(stream.Name, stream, null, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, null, description: description)); return this; } @@ -301,18 +234,18 @@ public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStream /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { - if (this.Files.Count + files.Count > 10) + if (this.FilesInternal.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { - if (this._files.Any(x => x.Filename == file.Key)) + if (this.FilesInternal.Any(x => x.Filename == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(file.Key, file.Value, file.Value.Position)); + this.FilesInternal.Add(new(file.Key, file.Value, file.Value.Position)); else - this._files.Add(new(file.Key, file.Value, null)); + this.FilesInternal.Add(new(file.Key, file.Value, null)); } return this; @@ -321,28 +254,22 @@ public DiscordFollowupMessageBuilder AddFiles(Dictionary files, /// /// Adds the mention to the mentions to parse, etc. with the followup message. /// - /// Mention to add. + /// Mention to add. /// The builder to chain calls with. - public DiscordFollowupMessageBuilder WithAllowedMention(IMention allowedMention) + public DiscordFollowupMessageBuilder WithAllowedMention(IMention mention) { - if (this.Mentions != null) - this.Mentions.Add(allowedMention); - else - this.Mentions = [allowedMention]; + this.MentionsInternal.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the followup message. /// - /// Mentions to add. + /// Mentions to add. /// The builder to chain calls with. - public DiscordFollowupMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) + public DiscordFollowupMessageBuilder WithAllowedMentions(IEnumerable mentions) { - if (this.Mentions != null) - this.Mentions.AddRange(allowedMentions); - else - this.Mentions = allowedMentions.ToList(); + this.MentionsInternal.AddRange(mentions); return this; } @@ -351,7 +278,6 @@ public DiscordFollowupMessageBuilder WithAllowedMentions(IEnumerable a /// public DiscordFollowupMessageBuilder AsEphemeral() { - this.FlagsChanged = true; this.IsEphemeral = true; return this; } @@ -361,7 +287,6 @@ public DiscordFollowupMessageBuilder AsEphemeral() /// public DiscordFollowupMessageBuilder SuppressEmbeds() { - this.FlagsChanged = true; this.EmbedsSuppressed = true; return this; } @@ -371,7 +296,6 @@ public DiscordFollowupMessageBuilder SuppressEmbeds() /// internal DiscordFollowupMessageBuilder AsVoiceMessage(bool asVoiceMessage = true) { - this.FlagsChanged = true; this.IsVoiceMessage = asVoiceMessage; return this; } @@ -381,45 +305,31 @@ internal DiscordFollowupMessageBuilder AsVoiceMessage(bool asVoiceMessage = true /// public DiscordFollowupMessageBuilder AsSilentMessage() { - this.FlagsChanged = true; this.NotificationsSuppressed = true; return this; } - /// - /// Clears all message components on this builder. - /// - public void ClearComponents() - => this._components.Clear(); - /// /// Clears the poll from this builder. /// public void ClearPoll() => this.Poll = null; - /// - /// Allows for clearing the Followup Message builder so that it can be used again to send a new message. - /// - public void Clear() + /// + public override void Clear() { - this.Content = null; - this._embeds.Clear(); this.IsTts = false; - this.Mentions = null; - this._files.Clear(); this.IsEphemeral = false; - this._components.Clear(); + base.Clear(); } - /// - /// Validates the builder. - /// - internal void Validate() + /// + internal override void Validate() { - if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any() && !this.Components.Any() && this.Poll is null && this?.Attachments.Count == 0) + if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && this.EmbedsInternal.Count == 0 && this.ComponentsInternal.Count == 0 && this.Poll is null && this.AttachmentsInternal.Count == 0) throw new ArgumentException("You must specify content, an embed, a component, a poll, or at least one file."); this.Poll?.Validate(); + base.Validate(); } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs index f8776f5019..4b58349ae9 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs @@ -33,7 +33,7 @@ internal class DiscordInteractionApplicationCommandCallbackData : ObservableApiO /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? Mentions { get; internal set; } + public DiscordMentions? Mentions { get; internal set; } /// /// Gets the flags. @@ -45,7 +45,7 @@ internal class DiscordInteractionApplicationCommandCallbackData : ObservableApiO /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection? Components { get; internal set; } + public IReadOnlyCollection? Components { get; internal set; } /// /// Gets the autocomplete choices. diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs index 6ac377b6d3..89d0c832d5 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; +using DisCatSharp.Entities.Core; + namespace DisCatSharp.Entities; /// @@ -11,7 +13,10 @@ public sealed class DiscordInteractionModalBuilder { private readonly List _callbackHints = []; - private readonly List _components = []; + /// + /// The components. + /// + internal readonly List ComponentsInternal = []; private string _title; @@ -24,6 +29,11 @@ public DiscordInteractionModalBuilder(string title = null, string customId = nul this.CustomId = customId ?? Guid.NewGuid().ToString(); } + /// + /// Components to send. Please use instead. + /// + public IReadOnlyList Components => this.ComponentsInternal; + /// /// Title of modal. /// @@ -47,7 +57,7 @@ public string Title /// /// Components to send on this interaction response. /// - public IReadOnlyList ModalComponents => this._components; + public IReadOnlyList ModalComponents => this.Components.Select(c => c as DiscordActionRowComponent).ToList()!; /// /// The hints to send on this interaction response. @@ -82,7 +92,7 @@ public DiscordInteractionModalBuilder WithCustomId(string customId) /// The hint builder. /// The current builder to chain calls with. /// Thrown when the is . - public DiscordInteractionModalBuilder WithCallbackHints(DiscordCallbackHintBuilder hintBuilder) + internal DiscordInteractionModalBuilder WithCallbackHints(DiscordCallbackHintBuilder hintBuilder) { if (hintBuilder == null) throw new ArgumentNullException(nameof(hintBuilder), "Callback hint builder cannot be null."); @@ -142,14 +152,12 @@ public DiscordInteractionModalBuilder AddModalComponents(params DiscordComponent throw new ArgumentException("You can only add 5 components to modals."); - if (this._components.Count + ara.Length > 5) - throw new ArgumentException($"You try to add too many components. We already have {this._components.Count}."); + if (this.ComponentsInternal.Count + ara.Length > 5) + throw new ArgumentException($"You try to add too many components. We already have {this.ComponentsInternal.Count}."); foreach (var ar in ara) - this._components.Add(new(new List - { - ar - })); + this.ComponentsInternal.Add(new DiscordActionRowComponent( + [ar])); return this; } @@ -164,11 +172,11 @@ public DiscordInteractionModalBuilder AddModalComponents(IEnumerable 5) + if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) - this._components.Add(ar); + this.ComponentsInternal.Add(ar); return this; } @@ -180,10 +188,8 @@ public DiscordInteractionModalBuilder AddModalComponents(IEnumerableThe current builder to chain calls with. internal DiscordInteractionModalBuilder AddModalComponents(DiscordComponent component) { - this._components.Add(new(new List - { - component - })); + this.ComponentsInternal.Add(new DiscordActionRowComponent( + [component])); return this; } @@ -192,14 +198,14 @@ internal DiscordInteractionModalBuilder AddModalComponents(DiscordComponent comp /// Clears all message components on this builder. /// public void ClearComponents() - => this._components.Clear(); + => this.ComponentsInternal.Clear(); /// /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. /// public void Clear() { - this._components.Clear(); + this.ComponentsInternal.Clear(); this.Title = null!; this.CustomId = null!; } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs index 66b3182792..1cc1338371 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs @@ -3,31 +3,19 @@ using System.IO; using System.Linq; +using DisCatSharp.Entities.Core; + namespace DisCatSharp.Entities; /// /// Constructs an interaction response. /// -public sealed class DiscordInteractionResponseBuilder +public sealed class DiscordInteractionResponseBuilder : DisCatSharpBuilder { - private readonly List _callbackHints = []; - - private readonly List _choices = []; - - private readonly List _components = []; - - private readonly List _embeds = []; - - private readonly List _files = []; - - internal readonly List AttachmentsInternal = []; - - private string _content; - /// - /// Whether flags were changed. + /// Gets the callback hints. /// - internal bool FlagsChanged = false; + private readonly List _callbackHints = []; /// /// Constructs a new empty interaction response builder. @@ -40,14 +28,23 @@ public DiscordInteractionResponseBuilder() /// . /// /// The builder to copy. - public DiscordInteractionResponseBuilder(DiscordMessageBuilder builder) + public DiscordInteractionResponseBuilder(DisCatSharpBuilder builder) { - this._content = builder.Content; - this.Mentions = builder.Mentions; - this._embeds.AddRange(builder.Embeds); - this._components.AddRange(builder.Components); + this.Content = builder.Content; + this.MentionsInternal.AddRange(builder.MentionsInternal); + this.EmbedsInternal.AddRange(builder.Embeds); + this.ComponentsInternal.AddRange(builder.Components); + this.EmbedsSuppressed = builder.EmbedsSuppressed; + this.IsComponentsV2 = builder.IsComponentsV2; + this.FilesInternal.AddRange(builder.Files); + this.AttachmentsInternal.AddRange(builder.Attachments); } + /// + /// Gets the choices. + /// + internal List ChoicesInternal { get; } = []; + /// /// Whether this interaction response is text-to-speech. /// @@ -68,102 +65,18 @@ public bool IsEphemeral private bool EPH { get; set; } - /// - /// Whether to suppress embeds. - /// - public bool EmbedsSuppressed - { - get => this.EMB_SUP; - set - { - this.EMB_SUP = value; - this.FlagsChanged = true; - } - } - - private bool EMB_SUP { get; set; } - - /// - /// Whether to send as silent message. - /// - public bool NotificationsSuppressed - { - get => this.NOTI_SUP; - set - { - this.NOTI_SUP = value; - this.FlagsChanged = true; - } - } - - private bool NOTI_SUP { get; set; } - - /// - /// Whether to send as voice message. - /// You can't use that on your own, it needs DisCatSharp.Experimental. - /// - internal bool IsVoiceMessage - { - get => this.VOICE_MSG; - set - { - this.VOICE_MSG = value; - this.FlagsChanged = true; - } - } - - private bool VOICE_MSG { get; set; } - - /// - /// Content of the message to send. - /// - public string Content - { - get => this._content; - set - { - if (value is { Length: > 2000 }) - throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); - - this._content = value; - } - } - - /// - /// Embeds to send on this interaction response. - /// - public IReadOnlyList Embeds => this._embeds; - - /// - /// Files to send on this interaction response. - /// - public IReadOnlyList Files => this._files; - - /// - /// Components to send on this interaction response. - /// - public IReadOnlyList Components => this._components; - /// /// The choices to send on this interaction response. /// Mutually exclusive with content, embed, and components. /// - public IReadOnlyList Choices => this._choices; - - /// - /// Attachments to be send with this interaction response. - /// - public IReadOnlyList Attachments => this.AttachmentsInternal; - - /// - /// Mentions to send on this interaction response. - /// - public List? Mentions { get; private set; } + public IReadOnlyList Choices + => this.ChoicesInternal; /// /// The hints to send on this interaction response. /// - public IReadOnlyList CallbackHints => this._callbackHints; + internal IReadOnlyList CallbackHints + => this._callbackHints; /// /// Gets the poll for this message. @@ -176,7 +89,7 @@ public string Content /// The hint builder. /// The current builder to chain calls with. /// Thrown when the is . - public DiscordInteractionResponseBuilder WithCallbackHints(DiscordCallbackHintBuilder hintBuilder) + internal DiscordInteractionResponseBuilder WithCallbackHints(DiscordCallbackHintBuilder hintBuilder) { if (hintBuilder == null) throw new ArgumentNullException(nameof(hintBuilder), "Callback hint builder cannot be null."); @@ -208,11 +121,11 @@ public DiscordInteractionResponseBuilder AddComponents(IEnumerable 5) + if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) - this._components.Add(ar); + this.ComponentsInternal.Add(ar); return this; } @@ -225,14 +138,35 @@ public DiscordInteractionResponseBuilder AddComponents(IEnumerableThrown when passing more than 5 components. public DiscordInteractionResponseBuilder AddComponents(IEnumerable components) { - var compArr = components.ToArray(); - var count = compArr.Length; + var cmpArr = components.ToArray(); + var count = cmpArr.Length; - if (count > 5) - throw new ArgumentException("Cannot add more than 5 components per action row!"); + if (this.IsComponentsV2) + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 10: + throw new ArgumentException("Cannot add more than 10 components!"); + } + + this.ComponentsInternal.AddRange(cmpArr); + } + else + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 5: + throw new ArgumentException("Cannot add more than 5 components per action row!"); + } + + var comp = new DiscordActionRowComponent(cmpArr); + this.ComponentsInternal.Add(comp); + } - var arc = new DiscordActionRowComponent(compArr); - this._components.Add(arc); return this; } @@ -264,18 +198,26 @@ public DiscordInteractionResponseBuilder WithTts(bool tts) /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AsEphemeral() { - this.FlagsChanged = true; this.IsEphemeral = true; return this; } + /// + /// Sets that this builder should be using UI Kit. + /// + /// The current builder to chain calls with. + public DiscordInteractionResponseBuilder WithV2Components() + { + this.IsComponentsV2 = true; + return this; + } + /// /// Sets the interaction response to suppress embeds. /// /// The current builder to chain calls with. public DiscordInteractionResponseBuilder SuppressEmbeds() { - this.FlagsChanged = true; this.EmbedsSuppressed = true; return this; } @@ -286,7 +228,6 @@ public DiscordInteractionResponseBuilder SuppressEmbeds() /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AsSilentMessage() { - this.FlagsChanged = true; this.NotificationsSuppressed = true; return this; } @@ -296,7 +237,6 @@ public DiscordInteractionResponseBuilder AsSilentMessage() /// internal DiscordInteractionResponseBuilder AsVoiceMessage(bool asVoiceMessage = true) { - this.FlagsChanged = true; this.IsVoiceMessage = asVoiceMessage; return this; } @@ -319,8 +259,8 @@ public DiscordInteractionResponseBuilder WithContent(string content) /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddEmbed(DiscordEmbed embed) { - if (embed != null) - this._embeds.Add(embed); // Interactions will 400 silently // + ArgumentNullException.ThrowIfNull(embed, nameof(embed)); + this.EmbedsInternal.Add(embed); return this; } @@ -331,7 +271,7 @@ public DiscordInteractionResponseBuilder AddEmbed(DiscordEmbed embed) /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddEmbeds(IEnumerable embeds) { - this._embeds.AddRange(embeds); + this.EmbedsInternal.AddRange(embeds); return this; } @@ -348,16 +288,16 @@ public DiscordInteractionResponseBuilder AddEmbeds(IEnumerable emb /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count >= 10) + if (this.FilesInternal.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == filename)) + if (this.FilesInternal.Any(x => x.Filename == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(filename, data, data.Position, description: description)); + this.FilesInternal.Add(new(filename, data, data.Position, description: description)); else - this._files.Add(new(filename, data, null, description: description)); + this.FilesInternal.Add(new(filename, data, null, description: description)); return this; } @@ -374,16 +314,16 @@ public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, b /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count >= 10) + if (this.FilesInternal.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == stream.Name)) + if (this.FilesInternal.Any(x => x.Filename == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(stream.Name, stream, stream.Position, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new(stream.Name, stream, null, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, null, description: description)); return this; } @@ -399,18 +339,18 @@ public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetSt /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { - if (this.Files.Count + files.Count > 10) + if (this.FilesInternal.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { - if (this._files.Any(x => x.Filename == file.Key)) + if (this.FilesInternal.Any(x => x.Filename == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(file.Key, file.Value, file.Value.Position)); + this.FilesInternal.Add(new(file.Key, file.Value, file.Value.Position)); else - this._files.Add(new(file.Key, file.Value, null)); + this.FilesInternal.Add(new(file.Key, file.Value, null)); } return this; @@ -419,28 +359,22 @@ public DiscordInteractionResponseBuilder AddFiles(Dictionary fil /// /// Adds the mention to the mentions to parse, etc. with the interaction response. /// - /// Mention to add. + /// Mention to add. /// The current builder to chain calls with. - public DiscordInteractionResponseBuilder WithAllowedMention(IMention allowedMention) + public DiscordInteractionResponseBuilder WithAllowedMention(IMention mention) { - if (this.Mentions != null) - this.Mentions.Add(allowedMention); - else - this.Mentions = [allowedMention]; + this.MentionsInternal.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the interaction response. /// - /// Mentions to add. + /// Mentions to add. /// The current builder to chain calls with. - public DiscordInteractionResponseBuilder WithAllowedMentions(IEnumerable allowedMentions) + public DiscordInteractionResponseBuilder WithAllowedMentions(IEnumerable mentions) { - if (this.Mentions != null) - this.Mentions.AddRange(allowedMentions); - else - this.Mentions = allowedMentions.ToList(); + this.MentionsInternal.AddRange(mentions); return this; } @@ -451,7 +385,7 @@ public DiscordInteractionResponseBuilder WithAllowedMentions(IEnumerableThe current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordApplicationCommandAutocompleteChoice choice) { - this._choices.Add(choice); + this.ChoicesInternal.Add(choice); return this; } @@ -462,7 +396,7 @@ public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordApplicatio /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices) { - this._choices.AddRange(choices); + this.ChoicesInternal.AddRange(choices); return this; } @@ -474,34 +408,18 @@ public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable this.AddAutoCompleteChoices((IEnumerable)choices); - /// - /// Clears all message components on this builder. - /// - public void ClearComponents() - => this._components.Clear(); - /// /// Clears the poll from this builder. /// public void ClearPoll() => this.Poll = null; - /// - /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. - /// - public void Clear() + /// + public override void Clear() { - this.Content = null; - this._embeds.Clear(); - this.IsTts = false; this.IsEphemeral = false; - this.Mentions = null; - this._components.Clear(); - this._choices.Clear(); - this._files.Clear(); + this.ChoicesInternal.Clear(); this.Poll = null; - this.IsVoiceMessage = false; - this.NotificationsSuppressed = false; - this.EmbedsSuppressed = false; + base.Clear(); } } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index d8a4f59c4d..7110a3e395 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -635,8 +635,8 @@ internal void PopulateMentions() if (!string.IsNullOrWhiteSpace(this.Content)) if (guild != null) { - this.MentionedRolesInternal = this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList(); - this.MentionedChannelsInternal = this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this.Content).Select(xid => guild.GetChannel(xid))).ToList(); + this.MentionedRolesInternal = [.. this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(guild.GetRole))!]; + this.MentionedChannelsInternal = [.. this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this.Content).Select(guild.GetChannel))!]; } this.MentionedUsersInternal = [.. mentionedUsers]; @@ -651,7 +651,7 @@ internal void PopulateMentions() /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default); + => this.Flags?.HasMessageFlag(MessageFlags.IsComponentsV2) ?? false ? throw new InvalidOperationException("UI Kit messages can not have content.") : this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, [], default); /// /// Edits the message. @@ -662,7 +662,7 @@ public Task ModifyAsync(Optional content) /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional embed = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr([]), this.GetMentions(), default, default, Array.Empty(), default); + => this.Flags?.HasMessageFlag(MessageFlags.IsComponentsV2) ?? false ? throw new InvalidOperationException("UI Kit messages can not have embeds.") : this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr([]), this.GetMentions(), default, default, [], default); /// /// Edits the message. @@ -674,7 +674,7 @@ public Task ModifyAsync(Optional embed = default) /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional embed = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr([]), this.GetMentions(), default, default, Array.Empty(), default); + => this.Flags?.HasMessageFlag(MessageFlags.IsComponentsV2) ?? false ? throw new InvalidOperationException("UI Kit messages can not have content or embeds.") : this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr([]), this.GetMentions(), default, default, [], default); /// /// Edits the message. @@ -686,7 +686,7 @@ public Task ModifyAsync(Optional content, OptionalThrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional> embeds = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default); + => this.Flags?.HasMessageFlag(MessageFlags.IsComponentsV2) ?? false ? throw new InvalidOperationException("UI Kit messages can not have content or embeds.") : this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, [], default); /// /// Edits the message. @@ -699,7 +699,7 @@ public Task ModifyAsync(Optional content, Optional ModifyAsync(DiscordMessageBuilder builder) { builder.Validate(true); - return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 + return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), Optional.Some(builder.Mentions.AsEnumerable()), builder.Components, builder.EmbedsSuppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value && this.Attachments is not null ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() @@ -737,7 +737,7 @@ public async Task ModifyAsync(Action acti var builder = new DiscordMessageBuilder(); action(builder); builder.Validate(true); - return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 + return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), Optional.Some(builder.Mentions.AsEnumerable()), builder.Components, builder.EmbedsSuppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value && this.Attachments is not null ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() @@ -983,7 +983,7 @@ private async Task> GetReactionsInternalAsync(Discord throw new ArgumentException("Cannot get a negative number of reactions' users."); if (limit == 0) - return Array.Empty(); + return []; var users = new List(limit); var remaining = limit; diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs index dc9640e599..fae2f4920c 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs @@ -4,65 +4,25 @@ using System.Linq; using System.Threading.Tasks; +using DisCatSharp.Attributes; +using DisCatSharp.Entities.Core; + namespace DisCatSharp.Entities; /// -/// Constructs a Message to be sent. +/// Constructs a message to be sent. /// -public sealed class DiscordMessageBuilder +public sealed class DiscordMessageBuilder : DisCatSharpBuilder { - private readonly List _embeds = []; - - internal readonly List AttachmentsInternal = []; - - internal readonly List ComponentsInternal = new(5); - - internal readonly List FilesInternal = []; - - private string _content; - /// /// Whether to keep previous attachments. /// internal bool? KeepAttachmentsInternal; - /// - /// Gets or Sets the Message to be sent. - /// - public string Content - { - get => this._content; - set - { - if (value is { Length: > 2000 }) - throw new ArgumentException("Content cannot exceed 2000 characters.", nameof(value)); - - this._content = value; - } - } - - /// - /// Gets or sets the embed for the builder. This will always set the builder to have one embed. - /// - public DiscordEmbed Embed - { - get => this._embeds.Count > 0 ? this._embeds[0] : null; - set - { - this._embeds.Clear(); - this._embeds.Add(value); - } - } - /// /// Gets the Sticker to be send. /// - public DiscordSticker Sticker { get; set; } - - /// - /// Gets the Embeds to be sent. - /// - public IReadOnlyList Embeds => this._embeds; + public DiscordSticker? Sticker { get; set; } /// /// Gets or Sets if the message should be TTS. @@ -74,32 +34,6 @@ public DiscordEmbed Embed /// public bool Silent { get; private set; } - /// - /// Whether to send as voice message. - /// You can't use that on your own, it needs DisCatSharp.Experimental. - /// - public bool IsVoiceMessage { get; private set; } - - /// - /// Gets the Allowed Mentions for the message to be sent. - /// - public List? Mentions { get; private set; } - - /// - /// Gets the Files to be sent in the Message. - /// - public IReadOnlyCollection Files => this.FilesInternal; - - /// - /// Gets the components that will be attached to the message. - /// - public IReadOnlyList Components => this.ComponentsInternal; - - /// - /// Gets the Attachments to be sent in the Message. - /// - public IReadOnlyList Attachments => this.AttachmentsInternal; - /// /// Gets the Reply Message ID. /// @@ -110,11 +44,6 @@ public DiscordEmbed Embed /// public bool MentionOnReply { get; private set; } - /// - /// Gets if the embeds should be suppressed. - /// - public bool Suppressed { get; private set; } - /// /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. /// If set to false, invalid replies are send as a regular message. @@ -239,16 +168,31 @@ public DiscordMessageBuilder AddComponents(IEnumerable compone var cmpArr = components.ToArray(); var count = cmpArr.Length; - switch (count) + if (this.IsComponentsV2) { - case 0: - throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); - case > 5: - throw new ArgumentException("Cannot add more than 5 components per action row!"); + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 10: + throw new ArgumentException("Cannot add more than 10 components!"); + } + + this.ComponentsInternal.AddRange(cmpArr); + } + else + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 5: + throw new ArgumentException("Cannot add more than 5 components per action row!"); + } + + var comp = new DiscordActionRowComponent(cmpArr); + this.ComponentsInternal.Add(comp); } - - var comp = new DiscordActionRowComponent(cmpArr); - this.ComponentsInternal.Add(comp); return this; } @@ -269,39 +213,35 @@ public DiscordMessageBuilder HasTts(bool isTts) /// public DiscordMessageBuilder SuppressEmbeds(bool suppress = true) { - this.Suppressed = suppress; + this.EmbedsSuppressed = suppress; return this; } /// - /// Sets the message to be send as voice message. + /// Sets that this builder should be using UI Kit. /// - public DiscordMessageBuilder AsVoiceMessage(bool asVoiceMessage = true) + /// The current builder to chain calls with. + public DiscordMessageBuilder WithV2Components() { - this.IsVoiceMessage = asVoiceMessage; + this.IsComponentsV2 = true; return this; } /// - /// Sets the followup message to be send as silent message. + /// Sets the message to be send as voice message. /// - public DiscordMessageBuilder AsSilentMessage(bool silent = true) + public DiscordMessageBuilder AsVoiceMessage(bool asVoiceMessage = true) { - this.Silent = silent; + this.IsVoiceMessage = asVoiceMessage; return this; } /// - /// Sets the embed for the current builder. + /// Sets the followup message to be send as silent message. /// - /// The embed that should be set. - /// The current builder to be chained. - public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) + public DiscordMessageBuilder AsSilentMessage(bool silent = true) { - if (embed == null) - return this; - - this.Embed = embed; + this.Silent = silent; return this; } @@ -312,10 +252,8 @@ public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) /// The current builder to be chained. public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) { - if (embed == null) - return this; //Providing null embeds will produce a 400 response from Discord.// - - this._embeds.Add(embed); + ArgumentNullException.ThrowIfNull(embed, nameof(embed)); + this.EmbedsInternal.Add(embed); return this; } @@ -326,42 +264,39 @@ public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) /// The current builder to be chained. public DiscordMessageBuilder AddEmbeds(IEnumerable embeds) { - this._embeds.AddRange(embeds); + this.EmbedsInternal.AddRange(embeds); return this; } /// /// Sets if the message has allowed mentions. /// - /// The allowed Mention that should be sent. + /// The allowed Mention that should be sent. /// The current builder to be chained. - public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) + public DiscordMessageBuilder WithAllowedMention(IMention mention) { - if (this.Mentions != null) - this.Mentions.Add(allowedMention); - else - this.Mentions = [allowedMention]; - + this.MentionsInternal.Add(mention); return this; } /// /// Sets if the message has allowed mentions. /// - /// The allowed Mentions that should be sent. + /// The allowed Mentions that should be sent. /// The current builder to be chained. - public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) + public DiscordMessageBuilder WithAllowedMentions(IEnumerable mentions) { - if (this.Mentions != null) - this.Mentions.AddRange(allowedMentions); - else - this.Mentions = allowedMentions.ToList(); - + this.MentionsInternal.AddRange(mentions); return this; } + /// + [Deprecated("Replaced by AddFile to streamline builders.")] + public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) + => this.AddFile(fileName, stream, resetStreamPosition, description); + /// - /// Sets if the message has files to be sent. + /// Adds a file to the message. /// /// The fileName that the file should be sent as. /// The Stream to the file. @@ -371,9 +306,9 @@ public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMe /// /// Description of the file. /// The current builder to be chained. - public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) + public DiscordMessageBuilder AddFile(string fileName, Stream stream, bool resetStreamPosition = false, string? description = null) { - if (this.Files.Count > 10) + if (this.FilesInternal.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.Filename == fileName)) @@ -387,8 +322,13 @@ public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool reset return this; } + /// + [Deprecated("Replaced by AddFile to streamline builders.")] + public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) + => this.AddFile(stream, resetStreamPosition, description); + /// - /// Sets if the message has files to be sent. + /// Adds a file to the message. /// /// The Stream to the file. /// @@ -397,9 +337,9 @@ public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool reset /// /// Description of the file. /// The current builder to be chained. - public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) + public DiscordMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string? description = null) { - if (this.Files.Count > 10) + if (this.FilesInternal.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.Filename == stream.Name)) @@ -413,8 +353,13 @@ public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPositio return this; } + /// + [Deprecated("Replaced by AddFiles to streamline builders.")] + public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) + => this.AddFiles(files, resetStreamPosition); + /// - /// Sets if the message has files to be sent. + /// Adds file to the message. /// /// The Files that should be sent. /// @@ -422,9 +367,9 @@ public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPositio /// sent. /// /// The current builder to be chained. - public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) + public DiscordMessageBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { - if (this.Files.Count + files.Count > 10) + if (this.FilesInternal.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) @@ -476,10 +421,7 @@ public DiscordMessageBuilder WithReply(ulong messageId, bool mention = false, bo this.FailOnInvalidReply = failOnInvalidReply; if (mention) - { - this.Mentions ??= []; - this.Mentions.Add(new RepliedUserMention()); - } + this.MentionsInternal.Add(new RepliedUserMention()); return this; } @@ -504,12 +446,6 @@ public Task SendAsync(DiscordChannel channel) public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); - /// - /// Clears all message components on this builder. - /// - public void ClearComponents() - => this.ComponentsInternal.Clear(); - /// /// Clears the poll from this builder. /// @@ -519,24 +455,17 @@ public void ClearPoll() /// /// Allows for clearing the Message Builder so that it can be used again to send a new message. /// - public void Clear() + public override void Clear() { - this.Content = ""; - this._embeds.Clear(); this.IsTts = false; - this.Mentions = null; - this.FilesInternal.Clear(); this.ReplyId = null; this.MentionOnReply = false; - this.ComponentsInternal.Clear(); - this.Suppressed = false; - this.Sticker = null; - this.AttachmentsInternal.Clear(); + this.Sticker = null!; this.KeepAttachmentsInternal = false; this.Nonce = null; this.EnforceNonce = false; this.Poll = null; - this.IsVoiceMessage = false; + base.Clear(); } /// @@ -545,7 +474,7 @@ public void Clear() /// Tells the method to perform the Modify Validation or Create Validation. internal void Validate(bool isModify = false) { - if (this._embeds.Count > 10) + if (this.EmbedsInternal.Count > 10) throw new ArgumentException("A message can only have up to 10 embeds."); if (isModify) @@ -553,14 +482,19 @@ internal void Validate(bool isModify = false) if (!isModify) { - if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null && (!this.Components?.Any() ?? true) && this.Poll is null && this?.Attachments.Count == 0) + if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any() && this.Sticker is null && (!this.Components?.Any() ?? true) && this.Poll is null && this?.Attachments.Count == 0) throw new ArgumentException("You must specify content, an embed, a sticker, a component, a poll or at least one file."); - if (this.Components.Count > 5) - throw new InvalidOperationException("You can only have 5 action rows per message."); + if (this.IsComponentsV2 && (!string.IsNullOrEmpty(this.Content) || this.Embeds.Any())) + throw new ArgumentException("Using UI Kit mode. You cannot specify content or embeds."); - if (this.Components.Any(c => c.Components.Count > 5)) - throw new InvalidOperationException("Action rows can only have 5 components"); + switch (this.IsComponentsV2) + { + case true when this.Components?.Count > 10: + throw new InvalidOperationException("You can only have 10 components per message."); + case false when this.Components?.Count > 5: + throw new InvalidOperationException("You can only have 5 action rows per message."); + } if (this.EnforceNonce && string.IsNullOrEmpty(this.Nonce)) throw new InvalidOperationException("Nonce enforcement is enabled, but no nonce is set."); @@ -569,5 +503,7 @@ internal void Validate(bool isModify = false) } else if (this.Poll is not null) throw new InvalidOperationException("Messages with polls can't be edited."); + + base.Validate(); } } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs index ff9f9838bd..2bceabd79e 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; +using DisCatSharp.Entities.Core; using DisCatSharp.Enums; namespace DisCatSharp.Entities; @@ -11,25 +12,10 @@ namespace DisCatSharp.Entities; /// /// Constructs ready-to-send webhook requests. /// -public sealed class DiscordWebhookBuilder +public sealed class DiscordWebhookBuilder : DisCatSharpBuilder { private readonly List _appliedTags = []; - private readonly List _components = []; - - private readonly List _embeds = []; - - private readonly List _files = []; - - private string _content; - - internal readonly List AttachmentsInternal = []; - - /// - /// Whether flags were changed. - /// - internal bool FlagsChanged = false; - /// /// Whether to keep previous attachments. /// @@ -39,7 +25,7 @@ public sealed class DiscordWebhookBuilder /// Constructs a new empty webhook request builder. /// public DiscordWebhookBuilder() - { } // I still see no point in initializing collections with empty collections. // + { } /// /// Username to use for this webhook request. @@ -56,68 +42,12 @@ public DiscordWebhookBuilder() /// public bool IsTts { get; private set; } - /// - /// Whether to suppress embeds. - /// - public bool EmbedsSuppressed { get; private set; } - - /// - /// Whether to send as silent message. - /// - public bool NotificationsSuppressed { get; private set; } - - /// - /// Whether to send as voice message. - /// You can't use that on your own, it needs DisCatSharp.Experimental. - /// - public bool IsVoiceMessage { get; private set; } - - /// - /// Message to send on this webhook request. - /// - public string Content - { - get => this._content; - set - { - if (value is { Length: > 2000 }) - throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); - - this._content = value; - } - } - /// /// Name of the new thread. /// Only works if the webhook is send in a . /// public string? ThreadName { get; set; } - /// - /// Embeds to send on this webhook request. - /// - public IReadOnlyList Embeds => this._embeds; - - /// - /// Files to send on this webhook request. - /// - public IReadOnlyList Files => this._files; - - /// - /// Mentions to send on this webhook request. - /// - public List? Mentions { get; private set; } - - /// - /// Gets the components. - /// - public IReadOnlyList Components => this._components; - - /// - /// Attachments to keep on this webhook request. - /// - public IReadOnlyList Attachments => this.AttachmentsInternal; - /// /// Forum post tags to send on this webhook request. /// @@ -133,7 +63,6 @@ public string Content /// public DiscordWebhookBuilder SuppressEmbeds() { - this.FlagsChanged = true; this.EmbedsSuppressed = true; return this; } @@ -143,7 +72,6 @@ public DiscordWebhookBuilder SuppressEmbeds() /// public DiscordWebhookBuilder AsSilentMessage() { - this.FlagsChanged = true; this.NotificationsSuppressed = true; return this; } @@ -153,7 +81,6 @@ public DiscordWebhookBuilder AsSilentMessage() /// public DiscordWebhookBuilder AsVoiceMessage() { - this.FlagsChanged = true; this.IsVoiceMessage = true; return this; } @@ -176,11 +103,11 @@ public DiscordWebhookBuilder AddComponents(IEnumerable 5) + if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) - this._components.Add(ar); + this.ComponentsInternal.Add(ar); return this; } @@ -196,17 +123,43 @@ public DiscordWebhookBuilder AddComponents(IEnumerable compone var cmpArr = components.ToArray(); var count = cmpArr.Length; - switch (count) + if (this.IsComponentsV2) + { + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 10: + throw new ArgumentException("Cannot add more than 10 components!"); + } + + this.ComponentsInternal.AddRange(cmpArr); + } + else { - case 0: - throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); - case > 5: - throw new ArgumentException("Cannot add more than 5 components per action row!"); + switch (count) + { + case 0: + throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); + case > 5: + throw new ArgumentException("Cannot add more than 5 components per action row!"); + } + + var comp = new DiscordActionRowComponent(cmpArr); + this.ComponentsInternal.Add(comp); } - var comp = new DiscordActionRowComponent(cmpArr); - this._components.Add(comp); + return this; + } + /// + /// Sets that this builder should be using UI Kit. + /// + /// The current builder to chain calls with. + public DiscordWebhookBuilder WithV2Components() + { + this.FlagsChanged = true; + this.IsComponentsV2 = true; return this; } @@ -278,9 +231,8 @@ public DiscordWebhookBuilder WithThreadName(string name) /// Embed to add. public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) { - if (embed != null) - this._embeds.Add(embed); - + ArgumentNullException.ThrowIfNull(embed, nameof(embed)); + this.EmbedsInternal.Add(embed); return this; } @@ -290,7 +242,7 @@ public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) /// Embeds to add. public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) { - this._embeds.AddRange(embeds); + this.EmbedsInternal.AddRange(embeds); return this; } @@ -306,16 +258,16 @@ public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) /// Description of the file. public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count > 10) + if (this.FilesInternal.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == filename)) + if (this.FilesInternal.Any(x => x.Filename == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(filename, data, data.Position, description: description)); + this.FilesInternal.Add(new(filename, data, data.Position, description: description)); else - this._files.Add(new(filename, data, null, description: description)); + this.FilesInternal.Add(new(filename, data, null, description: description)); return this; } @@ -332,16 +284,16 @@ public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStr /// public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { - if (this.Files.Count > 10) + if (this.FilesInternal.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); - if (this._files.Any(x => x.Filename == stream.Name)) + if (this.FilesInternal.Any(x => x.Filename == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(stream.Name, stream, stream.Position, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, stream.Position, description: description)); else - this._files.Add(new(stream.Name, stream, null, description: description)); + this.FilesInternal.Add(new(stream.Name, stream, null, description: description)); return this; } @@ -356,18 +308,18 @@ public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition /// public DiscordWebhookBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { - if (this.Files.Count + files.Count > 10) + if (this.FilesInternal.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { - if (this._files.Any(x => x.Filename == file.Key)) + if (this.FilesInternal.Any(x => x.Filename == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) - this._files.Add(new(file.Key, file.Value, file.Value.Position)); + this.FilesInternal.Add(new(file.Key, file.Value, file.Value.Position)); else - this._files.Add(new(file.Key, file.Value, null)); + this.FilesInternal.Add(new(file.Key, file.Value, null)); } return this; @@ -397,26 +349,20 @@ public DiscordWebhookBuilder KeepAttachments(bool keep) /// /// Adds the mention to the mentions to parse, etc. at the execution of the webhook. /// - /// Mention to add. - public DiscordWebhookBuilder WithAllowedMention(IMention allowedMention) + /// Mention to add. + public DiscordWebhookBuilder WithAllowedMention(IMention mention) { - if (this.Mentions != null) - this.Mentions.Add(allowedMention); - else - this.Mentions = [allowedMention]; + this.MentionsInternal.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. at the execution of the webhook. /// - /// Mentions to add. - public DiscordWebhookBuilder WithAllowedMentions(IEnumerable allowedMentions) + /// Mentions to add. + public DiscordWebhookBuilder WithAllowedMentions(IEnumerable mentions) { - if (this.Mentions != null) - this.Mentions.AddRange(allowedMentions); - else - this.Mentions = allowedMentions.ToList(); + this.MentionsInternal.AddRange(mentions); return this; } @@ -435,7 +381,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// /// The webhook that should be executed. /// The message sent - public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this).ConfigureAwait(false); + public async Task SendAsync(DiscordWebhook webhook) + => await webhook.ExecuteAsync(this).ConfigureAwait(false); /// /// Executes a webhook. @@ -443,7 +390,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// The webhook that should be executed. /// Target thread id. /// The message sent - public async Task SendAsync(DiscordWebhook webhook, ulong threadId) => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); + public async Task SendAsync(DiscordWebhook webhook, ulong threadId) + => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); /// /// Sends the modified webhook message. @@ -451,7 +399,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// The webhook that should be executed. /// The message to modify. /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); + public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) + => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. @@ -459,7 +408,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// The webhook that should be executed. /// The id of the message to modify. /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); + public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) + => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); /// /// Sends the modified webhook message. @@ -468,7 +418,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// The message to modify. /// Target thread. /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); + public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) + => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. @@ -477,13 +428,8 @@ public DiscordWebhookBuilder WithAppliedTags(IEnumerable tags) /// The id of the message to modify. /// Target thread id. /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); - - /// - /// Clears all message components on this builder. - /// - public void ClearComponents() - => this._components.Clear(); + public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) + => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); /// /// Clears the poll from this builder. @@ -494,22 +440,13 @@ public void ClearPoll() /// /// Allows for clearing the Webhook Builder so that it can be used again to send a new message. /// - public void Clear() + public override void Clear() { - this.Content = ""; - this._embeds.Clear(); this.IsTts = false; - this.Mentions = null; - this._files.Clear(); - this.AttachmentsInternal.Clear(); - this._components.Clear(); this.KeepAttachmentsInternal = false; this.ThreadName = null; this.Poll = null; - this.FlagsChanged = false; - this.NotificationsSuppressed = false; - this.IsTts = false; - this.IsVoiceMessage = false; + base.Clear(); } /// @@ -555,5 +492,7 @@ internal void Validate(bool isModify = false, bool isFollowup = false, bool isIn throw new ArgumentException("You must specify content, an embed, a component, a poll, or at least one file."); this.Poll?.Validate(); + + base.Validate(); } } diff --git a/DisCatSharp/Enums/Interaction/ComponentType.cs b/DisCatSharp/Enums/Interaction/ComponentType.cs index c1ecfc4c33..12864c84c4 100644 --- a/DisCatSharp/Enums/Interaction/ComponentType.cs +++ b/DisCatSharp/Enums/Interaction/ComponentType.cs @@ -45,8 +45,43 @@ public enum ComponentType /// ChannelSelect = 8, + /// + /// A section. + /// + Section = 9, + + /// + /// A text display. + /// + TextDisplay = 10, + + /// + /// A thumbnail. + /// + Thumbnail = 11, + + /// + /// A media gallery. + /// + MediaGallery = 12, + /// /// A file. /// - File = 13 + File = 13, + + /// + /// A separator. + /// + Separator = 14, + + /// + /// Cannot be used by bots. + /// + ContentInventoryEntry = 15, + + /// + /// A container. + /// + Container = 17 } diff --git a/DisCatSharp/Enums/Interaction/SeparatorSpacingSize.cs b/DisCatSharp/Enums/Interaction/SeparatorSpacingSize.cs new file mode 100644 index 0000000000..b279af4885 --- /dev/null +++ b/DisCatSharp/Enums/Interaction/SeparatorSpacingSize.cs @@ -0,0 +1,17 @@ +namespace DisCatSharp.Enums; + +/// +/// Represents the sperator spacing size of a . +/// +public enum SeparatorSpacingSize +{ + /// + /// A small spacing size. + /// + Small = 1, + + /// + /// A large spacing size. + /// + Large = 2 +} diff --git a/DisCatSharp/Enums/Message/MessageFlags.cs b/DisCatSharp/Enums/Message/MessageFlags.cs index 2ea21e97e7..4bc53b490c 100644 --- a/DisCatSharp/Enums/Message/MessageFlags.cs +++ b/DisCatSharp/Enums/Message/MessageFlags.cs @@ -86,5 +86,10 @@ public enum MessageFlags /// /// The message is a voice message. /// - IsVoiceMessage = 1 << 13 + IsVoiceMessage = 1 << 13, + + /// + /// The message uses the UI Kit. + /// + IsComponentsV2 = 1 << 15 } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index b667cc1a98..b009199de6 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -253,7 +253,7 @@ internal sealed class RestFollowupMessageCreatePayload : ObservableApiObject /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } + public DiscordMentions? Mentions { get; set; } /// /// Gets the flags. @@ -265,7 +265,7 @@ internal sealed class RestFollowupMessageCreatePayload : ObservableApiObject /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection Components { get; set; } + public IReadOnlyCollection Components { get; set; } /// /// Gets attachments. diff --git a/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs index a890c6c672..64706c5292 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestChannelPayloads.cs @@ -280,7 +280,7 @@ internal class RestChannelMessageEditPayload : ObservableApiObject /// Gets or sets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection Components { get; set; } + public IReadOnlyCollection Components { get; set; } /// /// Gets or sets a value indicating whether has embed. diff --git a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs index 7caedb5cf4..f4cfeeb14d 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestWebhookPayloads.cs @@ -88,7 +88,7 @@ internal sealed class RestWebhookExecutePayload : ObservableApiObject /// Gets or sets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Components { get; set; } + public IEnumerable Components { get; set; } /// /// Gets or sets the attachments. @@ -136,7 +136,7 @@ internal sealed class RestWebhookMessageEditPayload : ObservableApiObject /// Gets or sets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Mentions { get; set; } + public DiscordMentions? Mentions { get; set; } /// /// Gets or sets the attachments. @@ -148,7 +148,7 @@ internal sealed class RestWebhookMessageEditPayload : ObservableApiObject /// Gets or sets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Components { get; set; } + public IEnumerable Components { get; set; } [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; set; } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index 140a7cb3e4..ff249d0758 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -105,7 +105,7 @@ internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRat /// /// The values. /// Whether this query will be transmitted via POST. - private static string BuildQueryString(IDictionary values, bool post = false) + private static string BuildQueryString(Dictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; @@ -130,7 +130,7 @@ private DiscordMessage PrepareMessage(JToken msgRaw) this.PopulateMessage(author, ret); var referencedMsg = msgRaw["referenced_message"]; - if (ret is { InternalReference: { Type: ReferenceType.Default }, MessageType: MessageType.Reply } && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) + if (ret is { InternalReference.Type: ReferenceType.Default, MessageType: MessageType.Reply } && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; @@ -1107,7 +1107,7 @@ internal async Task> GetGuildBansAsync(ulong guildId, xb.User = usr; return xb; }); - var bans = new ReadOnlyCollection(new List(bansRaw)); + var bans = new ReadOnlyCollection([.. bansRaw]); return bans; } @@ -1160,7 +1160,7 @@ internal async Task CreateGuildBulkBanAsync(ulong guildI var pld = new RestGuildBulkBanPayload { - UserIds = userIds.ToList(), + UserIds = [.. userIds], DeleteMessageSeconds = deleteMessageSeconds }; @@ -1501,18 +1501,18 @@ internal async Task GetGuildWidgetAsync(ulong guildId) ret.Guild = this.Discord.Guilds.ContainsKey(guildId) ? this.Discord.Guilds[guildId] : null; ret.Channels = ret.Guild == null - ? rawChannels.Select(r => new DiscordChannel + ? [.. rawChannels.Select(r => new DiscordChannel { Id = (ulong)r["id"], Name = r["name"].ToString(), Position = (int)r["position"] - }).ToList() - : rawChannels.Select(r => + })] + : [.. rawChannels.Select(r => { var c = ret.Guild.GetChannel((ulong)r["id"]); c.Position = (int)r["position"]; return c; - }).ToList(); + })]; return ret; } @@ -1589,7 +1589,7 @@ internal async Task> GetGuildTemplatesAsync( var templatesRaw = JsonConvert.DeserializeObject>(res.Response); - return new ReadOnlyCollection(new List(templatesRaw)); + return new ReadOnlyCollection([.. templatesRaw]); } /// @@ -3039,7 +3039,7 @@ internal async Task GetMessageAsync(ulong channelId, ulong messa /// Thrown when the exceeds 2000 characters or is empty and /// if neither content, sticker, components and embeds are definied.. /// - internal async Task CreateMessageAsync(ulong channelId, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply, ReadOnlyCollection? components = null) + internal async Task CreateMessageAsync(ulong channelId, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply, ReadOnlyCollection? components = null) { if (content is { Length: > 2000 }) throw new ArgumentException("Message content length cannot exceed 2000 characters."); @@ -3110,12 +3110,14 @@ internal async Task CreateMessageAsync(ulong channelId, DiscordM embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var flags = MessageFlags.None; - if (builder.Suppressed) + if (builder.EmbedsSuppressed) flags |= MessageFlags.SuppressedEmbeds; if (builder.Silent) flags |= MessageFlags.SuppressNotifications; if (builder.IsVoiceMessage) flags |= MessageFlags.IsVoiceMessage; + if (builder.IsComponentsV2) + flags |= MessageFlags.IsComponentsV2; var pld = new RestChannelMessageCreatePayload { @@ -3142,7 +3144,7 @@ internal async Task CreateMessageAsync(ulong channelId, DiscordM FailIfNotExists = builder.FailOnInvalidReply }; - pld.Mentions = new(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); + pld.Mentions = new(builder.Mentions.Count == 0 ? Mentions.All : builder.Mentions, builder.Mentions.Any(), builder.MentionOnReply); if (builder.Files.Count == 0) { @@ -3277,7 +3279,7 @@ internal async Task> GetGuildChannelsAsync(ulong g foreach (var ret in channelsRaw) ret.Initialize(this.Discord); - return new ReadOnlyCollection(new List(channelsRaw)); + return new ReadOnlyCollection([.. channelsRaw]); } /// @@ -3435,7 +3437,7 @@ internal async Task> GetChannelMessagesAsync(ulong foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); - return new ReadOnlyCollection(new List(msgs)); + return new ReadOnlyCollection([.. msgs]); } /// @@ -3472,7 +3474,7 @@ internal async Task GetChannelMessageAsync(ulong channelId, ulon /// The suppress_embed. /// The files. /// The attachments to keep. - internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, Optional suppressEmbed, IReadOnlyCollection files, Optional> attachments) + internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, Optional suppressEmbed, IReadOnlyCollection files, Optional> attachments) { if (embeds is { HasValue: true, Value: not null }) foreach (var embed in embeds.Value) @@ -3675,7 +3677,7 @@ internal async Task> GetAnswerVotersAsync(ulong voters.Add(usr); } - return new(new List(voters)); + return new([.. voters]); } /// @@ -3721,7 +3723,7 @@ internal async Task> GetChannelInvitesAsync(ulong c return xi; }); - return new ReadOnlyCollection(new List(invitesRaw)); + return new ReadOnlyCollection([.. invitesRaw]); } /// @@ -3860,7 +3862,7 @@ internal async Task> GetPinnedMessagesAsync(ulong foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); - return new ReadOnlyCollection(new List(msgs)); + return new ReadOnlyCollection([.. msgs]); } /// @@ -4210,7 +4212,7 @@ internal async Task> GetCurrentUserGuildsAsync(int l { var guildsRaw = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); var glds = guildsRaw.Select(xug => (this.Discord as DiscordClient)?.GuildsInternal[xug.Id]); - return new ReadOnlyCollection(new List(glds)); + return new ReadOnlyCollection([.. glds]); } return DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); @@ -4978,7 +4980,7 @@ internal async Task> GetChannelWebhooksAsync(ulong xw.ApiClient = this; return xw; }); - return webhooksRaw.ToList(); + return [.. webhooksRaw]; } /// @@ -5001,7 +5003,7 @@ internal async Task> GetGuildWebhooksAsync(ulong g xw.ApiClient = this; return xw; }); - return webhooksRaw.ToList(); + return [.. webhooksRaw]; } /// @@ -5207,7 +5209,7 @@ internal async Task ExecuteWebhookAsync(ulong webhookId, string DiscordPollRequest = builder.Poll?.Build() }; - if (builder.Mentions != null) + if (builder.Mentions.Any()) pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (builder.Files?.Count > 0) @@ -5247,7 +5249,7 @@ internal async Task ExecuteWebhookAsync(ulong webhookId, string } } - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.Files?.Count > 0 || builder.IsTts || builder.Mentions != null) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.Files?.Count > 0 || builder.IsTts || builder.Mentions.Any()) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; @@ -5342,16 +5344,20 @@ internal async Task EditWebhookMessageAsync(ulong webhookId, str flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; + if (builder.IsComponentsV2) + flags |= MessageFlags.IsComponentsV2; var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, - Mentions = builder.Mentions, Components = builder.Components, Flags = flags }; + if (builder.Mentions.Any()) + pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); + if (builder.Files?.Count > 0) { ulong fileId = 0; @@ -5662,7 +5668,7 @@ internal async Task> GetReactionsAsync(ulong channelI reacters.Add(usr); } - return new ReadOnlyCollection(new List(reacters)); + return new ReadOnlyCollection([.. reacters]); } /// @@ -6321,7 +6327,7 @@ internal async Task> GetApplicationEmojis var emojisRaw = JsonConvert.DeserializeObject(res.Response); - return this.Discord.UpdateCachedApplicationEmojis(emojisRaw?.Value("items")).Select(x => x.Value).ToList(); + return [.. this.Discord.UpdateCachedApplicationEmojis(emojisRaw?.Value("items")).Select(x => x.Value)]; } /// @@ -6490,7 +6496,7 @@ internal async Task> GetStickerPacksAsync() var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); - return ret.ToList(); + return [.. ret]; } /// @@ -6647,7 +6653,7 @@ internal async Task> GetGlobalApplicati var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); - return ret.ToList(); + return [.. ret]; } /// @@ -6684,7 +6690,7 @@ internal async Task> BulkOverwriteGloba var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); - return ret.ToList(); + return [.. ret]; } /// @@ -6848,7 +6854,7 @@ internal async Task> GetGuildApplicatio var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); - return ret.ToList(); + return [.. ret]; } /// @@ -6886,7 +6892,7 @@ internal async Task> BulkOverwriteGuild var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); - return ret.ToList(); + return [.. ret]; } /// @@ -7062,6 +7068,8 @@ internal async Task CreateInteractionRespons flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; + if (builder.IsComponentsV2) + flags |= MessageFlags.IsComponentsV2; } var data = builder is not null @@ -7070,7 +7078,7 @@ internal async Task CreateInteractionRespons Content = builder?.Content ?? null, Embeds = builder?.Embeds ?? null, IsTts = builder?.IsTts, - Mentions = builder?.Mentions ?? null, + Mentions = (builder?.Mentions.Any() ?? false) ? new(builder.Mentions, builder.Mentions.Count is not 0) : null, Flags = flags, Components = builder?.Components ?? null, Choices = null, @@ -7143,7 +7151,7 @@ internal async Task CreateInteractionRespons var values = new Dictionary(); if (builder is not null) - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts || builder.Mentions is not null || builder.Files?.Count > 0 || builder.Components?.Count > 0) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts || builder.Mentions.Any() || builder.Files?.Count > 0 || builder.Components?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; @@ -7296,6 +7304,8 @@ internal async Task CreateFollowupMessageAsync(ulong application flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; + if (builder.IsComponentsV2) + flags |= MessageFlags.IsComponentsV2; var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload @@ -7345,10 +7355,10 @@ internal async Task CreateFollowupMessageAsync(ulong application } } - if (builder.Mentions != null) + if (builder.Mentions.Any()) pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts || builder.Mentions != null || builder.Files?.Count > 0 || builder.Components?.Count > 0) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts || builder.Mentions.Any() || builder.Files?.Count > 0 || builder.Components?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; @@ -7669,7 +7679,7 @@ internal async Task> GetApplicationAssets asset.Application = application; } - return new ReadOnlyCollection(new List(assets)); + return new ReadOnlyCollection([.. assets]); } /// diff --git a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs index f1e57fe39e..9d1fb23d91 100644 --- a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs +++ b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs @@ -54,6 +54,12 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer ComponentType.RoleSelect => new DiscordRoleSelectComponent(), ComponentType.MentionableSelect => new DiscordMentionableSelectComponent(), ComponentType.ChannelSelect => new DiscordChannelSelectComponent(), + ComponentType.Section => new DiscordSectionComponent(), + ComponentType.TextDisplay => new DiscordTextDisplayComponent(), + ComponentType.Thumbnail => new DiscordThumnailComponent(), + ComponentType.MediaGallery => new DiscordMediaGalleryComponent(), + ComponentType.File => new DiscordFileDisplayComponent(), + ComponentType.Separator => new DiscordSeparatorComponent(), _ => new() { Type = type