diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 5b04de590b..d7a530bba8 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -127,7 +127,7 @@ public interface IDiscordInteraction : ISnowflakeEntity /// A task that represents an asynchronous send operation for delivering the message. /// Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Responds to this interaction with a file attachment. @@ -149,7 +149,8 @@ Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, /// #if NETCOREAPP3_0_OR_GREATER async Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(fileStream, fileName)) { @@ -158,7 +159,8 @@ async Task RespondWithFileAsync(Stream fileStream, string fileName, string text } #else Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null + , MessageFlags flags = MessageFlags.None); #endif /// /// Responds to this interaction with a file attachment. @@ -180,7 +182,8 @@ Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null /// #if NETCOREAPP3_0_OR_GREATER async Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(filePath, fileName)) { @@ -189,7 +192,8 @@ async Task RespondWithFileAsync(string filePath, string fileName = null, string } #else Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None); #endif /// /// Responds to this interaction with a file attachment. @@ -210,11 +214,13 @@ Task RespondWithFileAsync(string filePath, string fileName = null, string text = /// #if NETCOREAPP3_0_OR_GREATER Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => RespondWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None) + => RespondWithFilesAsync([attachment], text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); #else Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None); #endif /// /// Responds to this interaction with a collection of file attachments. @@ -234,7 +240,9 @@ Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] /// contains the sent message. /// Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None); + /// /// Sends a followup message for this interaction. /// @@ -252,7 +260,9 @@ Task RespondWithFilesAsync(IEnumerable attachments, string text /// contains the sent message. /// Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None); + /// /// Sends a followup message for this interaction. /// @@ -273,17 +283,19 @@ Task FollowupAsync(string text = null, Embed[] embeds = null, bool /// #if NETCOREAPP3_0_OR_GREATER async Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(fileStream, fileName)) { - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } #else Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None); #endif + /// /// Sends a followup message for this interaction. /// @@ -304,16 +316,17 @@ Task FollowupWithFileAsync(Stream fileStream, string fileName, str /// #if NETCOREAPP3_0_OR_GREATER async Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(filePath, fileName)) { - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } #else Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); #endif /// /// Sends a followup message for this interaction. @@ -334,11 +347,11 @@ Task FollowupWithFileAsync(string filePath, string fileName = null /// #if NETCOREAPP3_0_OR_GREATER Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); #else Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); #endif /// /// Sends a followup message for this interaction. @@ -358,7 +371,7 @@ Task FollowupWithFileAsync(FileAttachment attachment, string text /// contains the sent message. /// Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Gets the original response for this interaction. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs index 202a5687ff..855b61ee29 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ActionRowComponent.cs @@ -1,27 +1,27 @@ using System.Collections.Generic; -namespace Discord +namespace Discord; + +/// +/// Represents a Row for child components to live in. +/// +public class ActionRowComponent : IMessageComponent { - /// - /// Represents a Row for child components to live in. - /// - public class ActionRowComponent : IMessageComponent - { - /// - public ComponentType Type => ComponentType.ActionRow; + /// + public ComponentType Type => ComponentType.ActionRow; - /// - /// Gets the child components in this row. - /// - public IReadOnlyCollection Components { get; internal set; } + /// + public int? Id { get; internal set; } - internal ActionRowComponent() { } + /// + /// Gets the child components in this row. + /// + public IReadOnlyCollection Components { get; internal set; } - internal ActionRowComponent(List components) - { - Components = components; - } + internal ActionRowComponent() { } - string IMessageComponent.CustomId => null; + internal ActionRowComponent(IReadOnlyCollection components) + { + Components = components; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs index dbbe4e9956..66fb654665 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ActionRowBuilder.cs @@ -8,8 +8,12 @@ namespace Discord; /// /// Represents a class used to build Action rows. /// -public class ActionRowBuilder +public class ActionRowBuilder : IMessageComponentBuilder, IInteractableComponentContainer { + public ComponentType Type => ComponentType.ActionRow; + + public int? Id { get; set; } + /// /// The max amount of child components this row can hold. /// @@ -20,7 +24,7 @@ public class ActionRowBuilder /// /// cannot be null. /// count exceeds . - public List Components + public List Components { get => _components; set @@ -37,7 +41,21 @@ public List Components } } - private List _components = new List(); + + public ActionRowBuilder AddComponents(params IMessageComponentBuilder[] components) + { + foreach (var component in components) + AddComponent(component); + return this; + } + + public ActionRowBuilder WithComponents(IEnumerable components) + { + Components = components.ToList(); + return this; + } + + private List _components = new (); /// /// Adds a list of components to the current row. @@ -45,7 +63,7 @@ public List Components /// The list of components to add. /// /// The current builder. - public ActionRowBuilder WithComponents(List components) + public ActionRowBuilder WithComponents(List components) { Components = components; return this; @@ -57,7 +75,7 @@ public ActionRowBuilder WithComponents(List components) /// The component to add. /// Components count reached /// The current builder. - public ActionRowBuilder AddComponent(IMessageComponent component) + public ActionRowBuilder AddComponent(IMessageComponentBuilder component) { if (Components.Count >= MaxChildCount) throw new InvalidOperationException($"Components count reached {MaxChildCount}"); @@ -103,13 +121,11 @@ public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) { if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); - - var builtMenu = menu.Build(); - + if (Components.Count != 0) throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); - AddComponent(builtMenu); + AddComponent(menu); return this; } @@ -152,15 +168,13 @@ public ActionRowBuilder WithButton( /// The current builder. public ActionRowBuilder WithButton(ButtonBuilder button) { - var builtButton = button.Build(); - if (Components.Count >= 5) throw new InvalidOperationException($"Components count reached {MaxChildCount}"); if (Components.Any(x => x.Type.IsSelectType())) throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); - AddComponent(builtButton); + AddComponent(button); return this; } @@ -171,10 +185,11 @@ public ActionRowBuilder WithButton(ButtonBuilder button) /// A that can be used within a public ActionRowComponent Build() { - return new ActionRowComponent(_components); + return new ActionRowComponent(_components.Select(x => x.Build()).ToList()); } + IMessageComponent IMessageComponentBuilder.Build() => Build(); - internal bool CanTakeComponent(IMessageComponent component) + internal bool CanTakeComponent(IMessageComponentBuilder component) { switch (component.Type) { @@ -195,4 +210,11 @@ internal bool CanTakeComponent(IMessageComponent component) return false; } } + + + IComponentContainer IComponentContainer.AddComponent(IMessageComponentBuilder component) => AddComponent(component); + + IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); + + IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components); } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs index 4522eb0629..e6c56afe99 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs @@ -7,8 +7,10 @@ namespace Discord; /// /// Represents a class used to build 's. /// -public class ButtonBuilder +public class ButtonBuilder : IInteractableComponentBuilder { + public ComponentType Type => ComponentType.Button; + /// /// The max length of a . /// @@ -74,6 +76,8 @@ public string CustomId /// public ulong? SkuId { get; set; } + public int? Id { get; set; } + private string _label; private string _customId; @@ -92,7 +96,7 @@ public ButtonBuilder() { } /// The emote of this button. /// Disabled this button or not. /// The sku id of this button. - public ButtonBuilder(string label = null, string customId = null, ButtonStyle style = ButtonStyle.Primary, string url = null, IEmote emote = null, bool isDisabled = false, ulong? skuId = null) + public ButtonBuilder(string label = null, string customId = null, ButtonStyle style = ButtonStyle.Primary, string url = null, IEmote emote = null, bool isDisabled = false, ulong? skuId = null, int? id = null) { CustomId = customId; Style = style; @@ -101,6 +105,7 @@ public ButtonBuilder(string label = null, string customId = null, ButtonStyle st IsDisabled = isDisabled; Emote = emote; SkuId = skuId; + Id = id; } /// @@ -115,6 +120,7 @@ public ButtonBuilder(ButtonComponent button) IsDisabled = button.IsDisabled; Emote = button.Emote; SkuId = button.SkuId; + Id = button.Id; } /// @@ -316,6 +322,8 @@ public ButtonComponent Build() break; } - return new ButtonComponent(Style, Label, Emote, CustomId, Url, IsDisabled, SkuId); + return new ButtonComponent(Style, Label, Emote, CustomId, Url, IsDisabled, SkuId, Id); } + + IMessageComponent IMessageComponentBuilder.Build() => Build(); } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs index eeb7db8596..5193c45852 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs @@ -98,7 +98,7 @@ public ComponentBuilder RemoveComponentsOfType(ComponentType t) /// The current builder. public ComponentBuilder RemoveComponent(string customId) { - this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c.CustomId == customId)); + this.ActionRows.ForEach(ar => ar.Components.RemoveAll(c => c is IInteractableComponent i && i.CustomId == customId)); return this; } @@ -158,20 +158,15 @@ public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); - - var builtMenu = menu.Build(); - + if (_actionRows == null) { - _actionRows = new List - { - new ActionRowBuilder().AddComponent(builtMenu) - }; + _actionRows = [new ActionRowBuilder().AddComponent(menu)]; } else { if (_actionRows.Count == row) - _actionRows.Add(new ActionRowBuilder().AddComponent(builtMenu)); + _actionRows.Add(new ActionRowBuilder().AddComponent(menu)); else { ActionRowBuilder actionRow; @@ -183,12 +178,12 @@ public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) _actionRows.Add(actionRow); } - if (actionRow.CanTakeComponent(builtMenu)) - actionRow.AddComponent(builtMenu); + if (actionRow.CanTakeComponent(menu)) + actionRow.AddComponent(menu); else if (row < MaxActionRowCount) WithSelectMenu(menu, row + 1); else - throw new InvalidOperationException($"There is no more row to add a {nameof(builtMenu)}"); + throw new InvalidOperationException($"There is no more row to add a {nameof(menu)}"); } } @@ -243,19 +238,17 @@ public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) { Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - var builtButton = button.Build(); - if (_actionRows == null) { _actionRows = new List { - new ActionRowBuilder().AddComponent(builtButton) + new ActionRowBuilder().AddComponent(button) }; } else { if (_actionRows.Count == row) - _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); + _actionRows.Add(new ActionRowBuilder().AddComponent(button)); else { ActionRowBuilder actionRow; @@ -267,8 +260,8 @@ public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) _actionRows.Add(actionRow); } - if (actionRow.CanTakeComponent(builtButton)) - actionRow.AddComponent(builtButton); + if (actionRow.CanTakeComponent(button)) + actionRow.AddComponent(button); else if (row < MaxActionRowCount) WithButton(button, row + 1); else @@ -326,7 +319,7 @@ public MessageComponent Build() _actionRows.RemoveAt(i); return _actionRows != null - ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) + ? new MessageComponent(_actionRows.Select(x => x.Build()).OfType().ToList()) : MessageComponent.Empty; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderExtensions.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderExtensions.cs new file mode 100644 index 0000000000..2c930feb5c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderExtensions.cs @@ -0,0 +1,17 @@ +namespace Discord; + +public static class ComponentBuilderExtensions +{ + /// + /// Sets the custom id for the component. + /// + /// + /// The current builder. + /// + public static BuilderT WithId(this BuilderT builder, int? id) + where BuilderT : IMessageComponentBuilder + { + builder.Id = id; + return builder; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs new file mode 100644 index 0000000000..8c119a2810 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilderV2.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord; + +public class ComponentBuilderV2 : IStaticComponentContainer +{ + /// + /// Gets the maximum number of components that can be added to this container. + /// + public const int MaxComponents = 10; + + private List _components = new(); + + /// + public List Components + { + get => _components; + set + { + _components = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); + } + } + + /// + /// Initializes a new instance of . + /// + public ComponentBuilderV2() { } + + /// + public ComponentBuilderV2 AddComponent(IMessageComponentBuilder component) + { + Components.Add(component); + return this; + } + + /// + public ComponentBuilderV2 AddComponents(params IMessageComponentBuilder[] components) + { + foreach (var component in components) + Components.Add(component); + return this; + } + + /// + public ComponentBuilderV2 WithComponents(IEnumerable components) + { + Components = components.ToList(); + return this; + } + + /// + public MessageComponent Build() + { + if (_components.Count is 0 or >MaxComponents) + throw new InvalidOperationException($"The number of components must be between 1 and {MaxComponents}."); + + if (_components.Any(x => + x is not ActionRowBuilder + and not SectionBuilder + and not TextDisplayBuilder + and not MediaGalleryBuilder + and not FileComponentBuilder + and not SeparatorBuilder + and not ContainerBuilder)) + throw new InvalidOperationException($"Only the following components can be at the top level: {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components."); + + return new MessageComponent(Components.Select(x => x.Build()).ToList()); + } + + /// + IComponentContainer IComponentContainer.AddComponent(IMessageComponentBuilder component) => AddComponent(component); + + /// + IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); + + /// + IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs new file mode 100644 index 0000000000..bddf0137f1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs @@ -0,0 +1,315 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Discord; + +public static class ComponentContainerExtensions +{ + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithTextDisplay(this BuilderT container, TextDisplayBuilder textDisplay) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(textDisplay); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithTextDisplay(this BuilderT container, + string content, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithTextDisplay(new TextDisplayBuilder() + .WithContent(content) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSection(this BuilderT container, SectionBuilder section) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(section); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSection(this BuilderT container, + IEnumerable components, + IMessageComponentBuilder accessory, + bool isSpoiler = false, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithSection(new SectionBuilder() + .WithComponents(components) + .WithAccessory(accessory) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithMediaGallery(this BuilderT container, MediaGalleryBuilder mediaGallery) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(mediaGallery); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithMediaGallery(this BuilderT container, + IEnumerable items, + int? id = null) where BuilderT : class, IStaticComponentContainer + => container.WithMediaGallery(new MediaGalleryBuilder() + .WithItems(items) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithMediaGallery(this BuilderT container, + IEnumerable urls, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithMediaGallery(new MediaGalleryBuilder() + .WithItems(urls.Select(x => new MediaGalleryItemProperties(new UnfurledMediaItemProperties(x)))) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSeparator(this BuilderT container, SeparatorBuilder separator) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(separator); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSeparator(this BuilderT container, + SeparatorSpacingSize spacing = SeparatorSpacingSize.Small, + bool isDivider = true, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithSeparator(new SeparatorBuilder() + .WithSpacing(spacing) + .WithIsDivider(isDivider) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithFile(this BuilderT container, FileComponentBuilder file) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(file); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithFile(this BuilderT container, + UnfurledMediaItemProperties file, + bool isSpoiler = false, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithFile(new FileComponentBuilder() + .WithFile(file) + .WithIsSpoiler(isSpoiler) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithContainer(this BuilderT container, ContainerBuilder containerComponent) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(containerComponent); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithContainer(this BuilderT container, + IEnumerable components, + Color? accentColor = null, + bool isSpoiler = false, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithContainer(new ContainerBuilder() + .WithComponents(components) + .WithAccentColor(accentColor) + .WithSpoiler(isSpoiler) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithContainer(this BuilderT container, + params IMessageComponentBuilder[] components) + where BuilderT : class, IStaticComponentContainer + => container.WithContainer(new ContainerBuilder() + .WithComponents(components)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithButton(this BuilderT container, ButtonBuilder button) + where BuilderT : class, IInteractableComponentContainer + { + container.AddComponent(button); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithButton(this BuilderT container, + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false, + ulong? skuId = null, + int? id = null) + where BuilderT : class, IInteractableComponentContainer + => container.WithButton(new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled) + .WithSkuId(skuId) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSelectMenu(this BuilderT container, SelectMenuBuilder selectMenu) + where BuilderT : class, IInteractableComponentContainer + { + container.AddComponent(selectMenu); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithSelectMenu(this BuilderT container, + string customId, + List options = null, + string placeholder = null, + int minValues = 1, + int maxValues = 1, + bool disabled = false, + int row = 0, + ComponentType type = ComponentType.SelectMenu, + ChannelType[] channelTypes = null, + SelectMenuDefaultValue[] defaultValues = null, + int? id = null) + where BuilderT : class, IInteractableComponentContainer + => container.WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes) + .WithDefaultValues(defaultValues) + .WithId(id)); + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithActionRow(this BuilderT container, ActionRowBuilder actionRow) + where BuilderT : class, IStaticComponentContainer + { + container.AddComponent(actionRow); + return container; + } + + /// + /// Adds a to the container. + /// + /// + /// The current container. + /// + public static BuilderT WithActionRow(this BuilderT container, + IEnumerable components, + int? id = null) + where BuilderT : class, IStaticComponentContainer + => container.WithActionRow(new ActionRowBuilder() + .WithComponents(components) + .WithId(id)); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs new file mode 100644 index 0000000000..fe179868ea --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ContainerBuilder.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord; + +public class ContainerBuilder : IMessageComponentBuilder, IStaticComponentContainer +{ + /// + /// The maximum number of components allowed in a container. + /// + public const int MaxComponents = 10; + + /// + public ComponentType Type => ComponentType.Container; + + /// + public int? Id { get; set; } + + private List _components = new(); + + /// + public List Components + { + get => _components; + set => _components = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); + } + + /// + /// Gets or sets the accent color of this container. + /// + public uint? AccentColor { get; set; } + + /// + /// Gets or sets whether this container is a spoiler. + /// + public bool? IsSpoiler { get; set; } + + /// + /// Sets the accent color of this container. + /// + /// + /// The current builder. + /// + public ContainerBuilder WithAccentColor(Color? color) + { + AccentColor = color?.RawValue; + return this; + } + + /// + /// Sets whether this container is a spoiler. + /// + /// + /// The current builder. + /// + public ContainerBuilder WithSpoiler(bool isSpoiler) + { + IsSpoiler = isSpoiler; + return this; + } + + /// + public ContainerBuilder AddComponent(IMessageComponentBuilder component) + { + Components.Add(component); + return this; + } + + /// + public ContainerBuilder AddComponents(params IMessageComponentBuilder[] components) + { + foreach (var component in components) + Components.Add(component); + return this; + } + + /// + public ContainerBuilder WithComponents(IEnumerable components) + { + Components = components.ToList(); + return this; + } + + /// + public ContainerComponent Build() + { + if (_components.Count is 0 or > MaxComponents) + throw new InvalidOperationException($"A container must have between 1 and {MaxComponents} components."); + + if (_components.Any(x => x + is not ActionRowBuilder + and not TextDisplayBuilder + and not SectionBuilder + and not MediaGalleryBuilder + and not SeparatorBuilder + and not FileComponentBuilder)) + throw new InvalidOperationException($"A container can only contain {nameof(ActionRowBuilder)}, {nameof(TextDisplayBuilder)}, {nameof(SectionBuilder)}, {nameof(MediaGalleryBuilder)}, {nameof(SeparatorBuilder)}, or {nameof(FileComponentBuilder)} components."); + + return new(Components.ConvertAll(x => x.Build()).ToImmutableArray(), AccentColor, IsSpoiler, Id); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); + /// + IComponentContainer IComponentContainer.AddComponent(IMessageComponentBuilder component) => AddComponent(component); + /// + IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); + /// + IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs new file mode 100644 index 0000000000..d2851b7bef --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs @@ -0,0 +1,79 @@ +using System; + +namespace Discord; + +public class FileComponentBuilder : IMessageComponentBuilder +{ + /// + public ComponentType Type => ComponentType.File; + + /// + public int? Id { get; set; } + + /// + /// Gets or sets the file for the component. + /// + /// + /// Only attachment URLs are supported. + /// + public UnfurledMediaItemProperties File { get; set; } + + /// + /// Gets or sets whether the file is a spoiler. + /// + public bool? IsSpoiler { get; set; } + + /// + /// Initializes a new instance of the . + /// + public FileComponentBuilder() {} + + /// + /// Initializes a new instance of the . + /// + public FileComponentBuilder(UnfurledMediaItemProperties media, bool isSpoiler = false, int? id = null) + { + File = media; + Id = id; + IsSpoiler = isSpoiler; + } + + /// + /// Sets the file for the component. + /// + /// + /// The current builder. + /// + public FileComponentBuilder WithFile(UnfurledMediaItemProperties file) + { + File = file; + return this; + } + + /// + /// Sets whether the file is a spoiler. + /// + /// + /// The current builder. + /// + public FileComponentBuilder WithIsSpoiler(bool? isSpoiler) + { + IsSpoiler = isSpoiler; + return this; + } + + /// + public FileComponent Build() + { + if (string.IsNullOrWhiteSpace(File.Url)) + throw new InvalidOperationException("File URL must be set."); + + if (!File.Url.StartsWith("attachment://")) + throw new InvalidOperationException("File URL must be an attachment URL."); + + return new(new UnfurledMediaItem(File.Url), IsSpoiler, Id); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs new file mode 100644 index 0000000000..2c2734fb7c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IComponentContainer.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents a container with child components. +/// +public interface IComponentContainer +{ + /// + /// Gets the components in the container. + /// + List Components { get; } + + /// + /// Adds a component to the container. + /// + /// + /// The current container. + /// + IComponentContainer AddComponent(IMessageComponentBuilder component); + + /// + /// Adds components to the container. + /// + /// + /// The current container. + /// + IComponentContainer AddComponents(params IMessageComponentBuilder[] components); + + /// + /// Sets the components in the container. + /// + /// + /// The current container. + /// + IComponentContainer WithComponents(IEnumerable components); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentBuilder.cs new file mode 100644 index 0000000000..242060fcdd --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentBuilder.cs @@ -0,0 +1,12 @@ +namespace Discord; + +/// +/// Represents a builder for an interactable component. +/// +public interface IInteractableComponentBuilder : IMessageComponentBuilder +{ + /// + /// Gets or sets the custom id for the component. + /// + string CustomId { get; set; } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentContainer.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentContainer.cs new file mode 100644 index 0000000000..c86a189e79 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IInteractableComponentContainer.cs @@ -0,0 +1,9 @@ +namespace Discord; + +/// +/// Represents a container for interactable components. +/// +public interface IInteractableComponentContainer : IComponentContainer +{ + +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IMessageComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IMessageComponentBuilder.cs new file mode 100644 index 0000000000..88305308c1 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IMessageComponentBuilder.cs @@ -0,0 +1,19 @@ +namespace Discord; + +public interface IMessageComponentBuilder +{ + /// + /// Gets the type of the component. + /// + ComponentType Type { get; } + + /// + /// Gets or sets the id for the component. An autoincremented id will be assigned if not set. + /// + int? Id { get; set; } + + /// + /// Runs validation checks and builds the component. + /// + IMessageComponent Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IStaticComponentContainer.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IStaticComponentContainer.cs new file mode 100644 index 0000000000..829b77bd09 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/IStaticComponentContainer.cs @@ -0,0 +1,9 @@ +namespace Discord; + +/// +/// Represents a container for static components. +/// +public interface IStaticComponentContainer : IComponentContainer +{ + +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs new file mode 100644 index 0000000000..b6121acc35 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord; + +public class MediaGalleryBuilder : IMessageComponentBuilder +{ + /// + /// Gets the maximum number of items that can be added to a media gallery. + /// + public const int MaxItems = 10; + + /// + public ComponentType Type => ComponentType.MediaGallery; + + /// + public int? Id { get; set; } + + private List _items = new(); + + /// + /// Initializes a new instance of the . + /// + public MediaGalleryBuilder() { } + + /// + /// Initializes a new instance of the . + /// + public MediaGalleryBuilder(IEnumerable items, int? id = null) + { + Items = items.ToList(); + Id = id; + } + + /// + /// Gets or sets the items in this media gallery. + /// + public List Items + { + get => _items; + set => _items = value; + } + + /// + /// Adds a new item to the media gallery. + /// + /// + /// The current builder. + /// + public MediaGalleryBuilder AddItem(MediaGalleryItemProperties item) + { + _items.Add(item); + return this; + } + + /// + /// Adds a new item to the media gallery. + /// + /// + /// The current builder. + /// + public MediaGalleryBuilder AddItem(string url, string description = null, bool isSpoiler = false) + { + _items.Add(new MediaGalleryItemProperties(new UnfurledMediaItemProperties(url), description, isSpoiler)); + return this; + } + + /// + /// Adds a list of items to the media gallery. + /// + /// + /// The current builder. + /// + public MediaGalleryBuilder AddItems(params IEnumerable items) + { + foreach (var item in items) + _items.Add(item); + return this; + } + + /// + /// Sets the items in the media gallery. + /// + /// + /// The current builder. + /// + public MediaGalleryBuilder WithItems(IEnumerable items) + { + _items = items.ToList(); + return this; + } + + /// + public MediaGalleryComponent Build() + { + if (_items.Any(x => (x.Description?.Length ?? 0) > MediaGalleryItemProperties.MaxDescriptionLength)) + throw new ArgumentException($"{nameof(MediaGalleryItemProperties)} description length cannot exceed {MediaGalleryItemProperties.MaxDescriptionLength} characters."); + + if (_items.Any(x => !(x.Media.Url?.StartsWith("http://") ?? false) + && !(x.Media.Url?.StartsWith("https://") ?? false) + && !(x.Media.Url?.StartsWith("attachment://") ?? false))) + throw new ArgumentException($"{nameof(MediaGalleryItemProperties)} description must be a valid URL or attachment."); + + if (_items.Count is 0 or > MaxItems) + throw new ArgumentOutOfRangeException(nameof(Items), $"Media gallery items count must be in range [1, {MaxItems}]"); + + return new(_items.Select(x => new MediaGalleryItem(new UnfurledMediaItem(x.Media.Url), x.Description, x.IsSpoiler)).ToImmutableArray(), Id); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryItemProperties.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryItemProperties.cs new file mode 100644 index 0000000000..32d7d97aa8 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryItemProperties.cs @@ -0,0 +1,39 @@ +namespace Discord; + +public struct MediaGalleryItemProperties +{ + /// + /// The maximum length of the description. + /// + public const int MaxDescriptionLength = 256; + + /// + /// Gets or sets the media item to display. + /// + public UnfurledMediaItemProperties Media { get; set; } + + /// + /// Gets or sets the description of the media item. + /// + public string Description { get; set; } + + /// + /// Gets or sets whether the media item is a spoiler. + /// + public bool IsSpoiler { get; set; } + + /// + /// Initializes a new instance of the . + /// + public MediaGalleryItemProperties() { } + + /// + /// Initializes a new instance of the . + /// + public MediaGalleryItemProperties(UnfurledMediaItemProperties media, string description = null, bool isSpoiler = false) + { + Media = media; + Description = description; + IsSpoiler = isSpoiler; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs new file mode 100644 index 0000000000..6c9727f974 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SectionBuilder.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord; + +public class SectionBuilder : IMessageComponentBuilder, IStaticComponentContainer +{ + /// + /// Gets the maximum number of components allowed in this container. + /// + public const int MaxComponents = 3; + + /// + public ComponentType Type => ComponentType.Section; + + /// + public int? Id { get; set; } + + /// + /// Gets or sets the accessory component. + /// + /// + /// Only supports and currently. + /// + public IMessageComponentBuilder Accessory { get; set; } + + private List _components = new(); + + /// + /// + /// Only is supported. + /// + public List Components + { + get => _components; + set => _components = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Components)} cannot be null."); + } + + /// + /// + /// Only is supported. + /// + public SectionBuilder AddComponent(IMessageComponentBuilder component) + { + Components.Add(component); + return this; + } + + /// + /// + /// Only is supported. + /// + public SectionBuilder AddComponents(params IMessageComponentBuilder[] components) + { + foreach (var component in components) + AddComponent(component); + return this; + } + + /// + /// + /// Only is supported. + /// + public SectionBuilder WithComponents(IEnumerable components) + { + Components = components.ToList(); + return this; + } + + /// + /// Sets the accessory component. + /// + public SectionBuilder WithAccessory(IMessageComponentBuilder accessory) + { + Accessory = accessory; + return this; + } + + /// + public SectionComponent Build() + { + if (_components.Count is 0 or > MaxComponents) + throw new InvalidOperationException($"Section component can only contain {MaxComponents} child components!"); + + if (_components.Any(x => x is not TextDisplayBuilder)) + throw new InvalidOperationException($"Section component can only contain {nameof(TextDisplayBuilder)}!"); + + if (Accessory is null) + throw new ArgumentNullException(nameof(Accessory), "A section must have an accessory"); + + if (Accessory is not ButtonBuilder and not ThumbnailBuilder) + throw new InvalidOperationException($"Accessory component can only be {nameof(ButtonBuilder)} or {nameof(ThumbnailBuilder)}!"); + + return new(Id, Components.Select(x => x.Build()).ToImmutableArray(), Accessory?.Build()); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); + /// + IComponentContainer IComponentContainer.AddComponent(IMessageComponentBuilder component) => AddComponent(component); + /// + IComponentContainer IComponentContainer.AddComponents(params IMessageComponentBuilder[] components) => AddComponents(components); + /// + IComponentContainer IComponentContainer.WithComponents(IEnumerable components) => WithComponents(components.ToList()); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs index 000a1c6203..81f1def8a1 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SelectMenuBuilder.cs @@ -8,7 +8,7 @@ namespace Discord; /// /// Represents a class used to build 's. /// -public class SelectMenuBuilder +public class SelectMenuBuilder : IInteractableComponentBuilder { /// /// The max length of a . @@ -136,13 +136,16 @@ public List DefaultValues } } - private List _options = new List(); + /// + public int? Id { get; set; } + + private List _options = []; private int _minValues = 1; private int _maxValues = 1; private string _placeholder; private string _customId; private ComponentType _type = ComponentType.SelectMenu; - private List _defaultValues = new(); + private List _defaultValues = []; /// /// Creates a new instance of a . @@ -163,6 +166,7 @@ public SelectMenuBuilder(SelectMenuComponent selectMenu) .Select(x => new SelectMenuOptionBuilder(x.Label, x.Value, x.Description, x.Emote, x.IsDefault)) .ToList(); DefaultValues = selectMenu.DefaultValues?.ToList(); + Id = selectMenu.Id; } /// @@ -177,7 +181,7 @@ public SelectMenuBuilder(SelectMenuComponent selectMenu) /// The of this select menu. /// The types of channels this menu can select (only valid on s) public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, - bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null, List defaultValues = null) + bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null, List defaultValues = null, int? id = null) { CustomId = customId; Options = options; @@ -188,6 +192,7 @@ public SelectMenuBuilder(string customId, List options Type = type; ChannelTypes = channelTypes ?? new(); DefaultValues = defaultValues ?? new(); + Id = id; } /// @@ -401,6 +406,9 @@ public SelectMenuComponent Build() { var options = Options?.Select(x => x.Build()).ToList(); - return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes, DefaultValues); + return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, Id, ChannelTypes, DefaultValues); } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SeparatorBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SeparatorBuilder.cs new file mode 100644 index 0000000000..d308eddc60 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/SeparatorBuilder.cs @@ -0,0 +1,53 @@ +namespace Discord; + +public class SeparatorBuilder : IMessageComponentBuilder +{ + /// + public ComponentType Type => ComponentType.Separator; + + /// + /// Gets or sets whether the component is a divider. + /// + public bool? IsDivider { get; set; } + + /// + /// Gets or sets the spacing of the separator. + /// + public SeparatorSpacingSize? Spacing { get; set; } + + /// + public int? Id { get; set; } + + /// + /// Sets whether the component is a divider. + /// + /// + /// The current builder. + /// + public SeparatorBuilder WithIsDivider(bool? isDivider) + { + IsDivider = isDivider; + return this; + } + + /// + /// Sets the spacing of the separator. + /// + /// + /// The current builder. + /// + public SeparatorBuilder WithSpacing(SeparatorSpacingSize? spacing) + { + Spacing = spacing; + return this; + } + + /// + public SeparatorComponent Build() + { + return new(IsDivider, Spacing, Id); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs new file mode 100644 index 0000000000..eb2234eb42 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs @@ -0,0 +1,60 @@ +using System; + +namespace Discord; + +public class TextDisplayBuilder : IMessageComponentBuilder +{ + /// + /// The maximum length of the content. + /// + public const int MaxContentLength = 4096; + + /// + public ComponentType Type => ComponentType.ActionRow; + + /// + public int? Id { get; set; } + + /// + /// Gets or sets the content of the text display. + /// + public string Content { get; set; } + + /// + /// Initializes a new . + /// + public TextDisplayBuilder() { } + + /// + /// Initializes a new with the specified content. + /// + public TextDisplayBuilder(string content, int? id = null) + { + Content = content; + Id = id; + } + + /// + /// Sets the content of the text display. + /// + /// + /// The current builder. + /// + public TextDisplayBuilder WithContent(string content) + { + Content = content; + return this; + } + + /// + public TextDisplayComponent Build() + { + if (Content.Length > MaxContentLength) + throw new ArgumentException($"Content length must be less than or equal to {MaxContentLength}.", nameof(Content)); + + return new(Content, Id); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs index b675980bf6..dcdfc2cdda 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs @@ -6,8 +6,10 @@ namespace Discord; /// /// Represents a builder for creating a . /// -public class TextInputBuilder +public class TextInputBuilder : IInteractableComponentBuilder { + public ComponentType Type => ComponentType.TextInput; + /// /// The max length of a . /// @@ -99,6 +101,8 @@ public int? MaxLength /// public bool? Required { get; set; } + public int? Id { get; set; } + /// /// Gets or sets the default value of the text input. /// @@ -140,7 +144,7 @@ public string Value /// The text input's maximum length. /// The text input's required value. public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, - int? minLength = null, int? maxLength = null, bool? required = null, string value = null) + int? minLength = null, int? maxLength = null, bool? required = null, string value = null, int? id = null) { Label = label; Style = style; @@ -150,6 +154,7 @@ public TextInputBuilder(string label, string customId, TextInputStyle style = Te MaxLength = maxLength; Required = required; Value = value; + Id = id; } /// @@ -257,6 +262,8 @@ public TextInputComponent Build() if (Style is TextInputStyle.Short && Value?.Any(x => x == '\n') is true) throw new ArgumentException($"Value must not contain new line characters when style is {TextInputStyle.Short}.", nameof(Value)); - return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value); + return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value, Id); } + + IMessageComponent IMessageComponentBuilder.Build() => Build(); } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ThumbnailBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ThumbnailBuilder.cs new file mode 100644 index 0000000000..79a5fa05fe --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ThumbnailBuilder.cs @@ -0,0 +1,95 @@ +using System; + +namespace Discord; + +public class ThumbnailBuilder : IMessageComponentBuilder +{ + /// + /// Gets the maximum length of the description. + /// + public const int MaxDescriptionLength = 1024; + + /// + public ComponentType Type => ComponentType.Thumbnail; + + /// + public int? Id { get; set; } + + /// + /// Gets or sets the media of the thumbnail. + /// + public UnfurledMediaItemProperties Media { get; set; } + + /// + /// Gets or sets the description of the thumbnail. + /// + public string Description { get; set; } + + /// + /// Gets or sets whether the thumbnail is a spoiler. + /// + public bool IsSpoiler { get; set; } + + /// + /// Initializes a new instance of the . + /// + public ThumbnailBuilder() { } + + /// + /// Initializes a new instance of the . + /// + public ThumbnailBuilder(UnfurledMediaItemProperties media, string description = null, bool isSpoiler = false) + { + Media = media; + Description = description; + IsSpoiler = isSpoiler; + } + + /// + /// Sets the media of the thumbnail. + /// + /// + /// The current builder. + /// + public ThumbnailBuilder WithMedia(UnfurledMediaItemProperties media) + { + Media = media; + return this; + } + + /// + /// Sets the description of the thumbnail. + /// + /// + /// The current builder. + /// + public ThumbnailBuilder WithDescription(string description) + { + Description = description; + return this; + } + + /// + /// Sets whether the thumbnail is a spoiler. + /// + /// + /// The current builder. + /// + public ThumbnailBuilder WithSpoiler(bool isSpoiler) + { + IsSpoiler = isSpoiler; + return this; + } + + /// + public ThumbnailComponent Build() + { + if (Description is not null && Description.Length > MaxDescriptionLength) + throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); + + return new(Id, new UnfurledMediaItem(Media.Url), Description, IsSpoiler); + } + + /// + IMessageComponent IMessageComponentBuilder.Build() => Build(); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/UnfurledMediaItemProperties.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/UnfurledMediaItemProperties.cs new file mode 100644 index 0000000000..1bb2f85eb0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/UnfurledMediaItemProperties.cs @@ -0,0 +1,24 @@ +namespace Discord; + +public struct UnfurledMediaItemProperties +{ + /// + /// Gets or sets the URL of the media item. + /// + public string Url { get; set; } + + /// + /// Initializes a new instance of the . + /// + public UnfurledMediaItemProperties() {} + + /// + /// Initializes a new instance of the . + /// + public UnfurledMediaItemProperties(string url) + { + Url = url; + } + + public static implicit operator UnfurledMediaItemProperties(string url) => new(url); +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs index c387f9ad53..5a850e2015 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ButtonComponent.cs @@ -3,11 +3,14 @@ namespace Discord; /// /// Represents a Button. /// -public class ButtonComponent : IMessageComponent +public class ButtonComponent : IInteractableComponent { /// public ComponentType Type => ComponentType.Button; + /// + public int? Id { get; } + /// /// Gets the of this button, example buttons with each style can be found Here. /// @@ -56,9 +59,10 @@ public class ButtonComponent : IMessageComponent public ButtonBuilder ToBuilder() => new (Label, CustomId, Style, Url, Emote, IsDisabled); - internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled, ulong? skuId) + internal ButtonComponent(ButtonStyle style, string label, IEmote emote, string customId, string url, bool isDisabled, ulong? skuId, int? id) { Style = style; + Id = id; Label = label; Emote = emote; CustomId = customId; diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs index 0ad3f741a1..5d5cce5a77 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs @@ -44,5 +44,19 @@ public enum ComponentType /// A select menu for picking from channels. /// ChannelSelect = 8, + + Section = 9, + + TextDisplay = 10, + + Thumbnail = 11, + + MediaGallery = 12, + + File = 13, + + Separator = 14, + + Container = 17, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ContainerComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ContainerComponent.cs new file mode 100644 index 0000000000..79a40b153a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ContainerComponent.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents a container component. +/// +public class ContainerComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.Container; + + /// + public int? Id { get; } + + /// + /// Gets the components in this container. + /// + public IReadOnlyCollection Components { get; } + + /// + /// Gets the accent color of this container. + /// + public uint? AccentColor { get; } + + /// + /// Gets whether this container is a spoiler. + /// + public bool? IsSpoiler { get; } + + internal ContainerComponent(IReadOnlyCollection components, uint? accentColor, bool? isSpoiler, int? id = null) + { + Components = components; + AccentColor = accentColor; + IsSpoiler = isSpoiler; + Id = id; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileComponent.cs new file mode 100644 index 0000000000..91a3c75f8c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileComponent.cs @@ -0,0 +1,30 @@ +namespace Discord; + +/// +/// Represents a file component. +/// +public class FileComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.File; + + /// + public int? Id { get; } + + /// + /// Gets the file of this component. + /// + public UnfurledMediaItem File { get; } + + /// + /// Gets whether this file is a spoiler. + /// + public bool? IsSpoiler { get; } + + internal FileComponent(UnfurledMediaItem file, bool? isSpoiler, int? id = null) + { + File = file; + IsSpoiler = isSpoiler; + Id = id; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IInteractableComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IInteractableComponent.cs new file mode 100644 index 0000000000..013004908b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IInteractableComponent.cs @@ -0,0 +1,12 @@ +namespace Discord; + +/// +/// Represents a message component that can be interacted with. +/// +public interface IInteractableComponent : IMessageComponent +{ + /// + /// Gets the custom id of the component if possible; otherwise . + /// + string CustomId { get; } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs index 9366a44d69..6bc1c07d0d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs @@ -1,18 +1,14 @@ -namespace Discord +namespace Discord; + +/// +/// Represents a message component on a message. +/// +public interface IMessageComponent { /// - /// Represents a message component on a message. + /// Gets the of this Message Component. /// - public interface IMessageComponent - { - /// - /// Gets the of this Message Component. - /// - ComponentType Type { get; } + ComponentType Type { get; } - /// - /// Gets the custom id of the component if possible; otherwise . - /// - string CustomId { get; } - } + int? Id { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryComponent.cs new file mode 100644 index 0000000000..ce3c398917 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryComponent.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents a media gallery component. +/// +public class MediaGalleryComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.MediaGallery; + + /// + public int? Id { get; } + + /// + /// Gets the items in this media gallery. + /// + public IReadOnlyCollection Items { get; } + + internal MediaGalleryComponent(IReadOnlyCollection items, int? id) + { + Items = items; + Id = id; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryItem.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryItem.cs new file mode 100644 index 0000000000..da33f7c91c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MediaGalleryItem.cs @@ -0,0 +1,29 @@ +namespace Discord; + +/// +/// Represents a media gallery item. +/// +public readonly struct MediaGalleryItem +{ + /// + /// Gets the media for this item. + /// + public UnfurledMediaItem Media { get; } + + /// + /// Gets the description for this item. + /// + public string Description { get; } + + /// + /// Gets whether this item is a spoiler. + /// + public bool IsSpoiler { get; } + + internal MediaGalleryItem(UnfurledMediaItem media, string description, bool? isSpoiler) + { + Media = media; + Description = description; + IsSpoiler = isSpoiler ?? false; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs index 7205886816..18946a7380 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/MessageComponent.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; -namespace Discord +namespace Discord; + +/// +/// Represents a component object used to send components with messages. +/// +public class MessageComponent { /// - /// Represents a component object used to send components with messages. + /// Gets the components to be used in a message. /// - public class MessageComponent - { - /// - /// Gets the components to be used in a message. - /// - public IReadOnlyCollection Components { get; } + public IReadOnlyCollection Components { get; } - internal MessageComponent(List components) - { - Components = components; - } - - /// - /// Returns a empty . - /// - internal static MessageComponent Empty - => new MessageComponent(new List()); + internal MessageComponent(List components) + { + Components = components; } + + /// + /// Returns a empty . + /// + internal static MessageComponent Empty + => new([]); } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ResolvedUnfurledMediaItem.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ResolvedUnfurledMediaItem.cs new file mode 100644 index 0000000000..d7cfd1b754 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ResolvedUnfurledMediaItem.cs @@ -0,0 +1,41 @@ +namespace Discord; + +/// +/// Represents a media item that has been unfurled and resolved. +/// +public class ResolvedUnfurledMediaItem : UnfurledMediaItem +{ + /// + /// Gets the proxy URL for this media item. + /// + public string ProxyUrl { get; } + + /// + /// Gets the height of this media item. + /// + public int Height { get; } + + /// + /// Gets the width of this media item. + /// + public int Width { get; } + + /// + /// Gets the content type of this media item. + /// + public string ContentType { get;} + + /// + /// Gets the loading state of this media item. + /// + public UnfurledMediaItemLoadingState LoadingState { get; } + + internal ResolvedUnfurledMediaItem(string url, string proxyUrl, int height, int width, string contentType, UnfurledMediaItemLoadingState loadingState) : base(url) + { + ProxyUrl = proxyUrl; + Height = height; + Width = width; + ContentType = contentType; + LoadingState = loadingState; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SectionComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SectionComponent.cs new file mode 100644 index 0000000000..52d71e3419 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SectionComponent.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Discord; + +/// +/// Represents a section component. +/// +public class SectionComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.Section; + + /// + public int? Id { get; } + + /// + /// Gets the components in this section. + /// + public IReadOnlyCollection Components { get; } + + /// + /// Gets the accessory of this section. + /// + public IMessageComponent Accessory { get; } + + internal SectionComponent(int? id, IReadOnlyCollection components, IMessageComponent accessory) + { + Id = id; + Components = components; + Accessory = accessory; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs index 39631deea5..7d1a2558e3 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs @@ -7,11 +7,14 @@ namespace Discord /// /// Represents a select menu component defined at /// - public class SelectMenuComponent : IMessageComponent + public class SelectMenuComponent : IInteractableComponent { /// public ComponentType Type { get; } + /// + public int? Id { get; } + /// public string CustomId { get; } @@ -67,7 +70,7 @@ public SelectMenuBuilder ToBuilder() DefaultValues.ToList()); internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, - bool disabled, ComponentType type, IEnumerable channelTypes = null, IEnumerable defaultValues = null) + bool disabled, ComponentType type, int? id, IEnumerable channelTypes = null, IEnumerable defaultValues = null) { CustomId = customId; Options = options; @@ -76,8 +79,9 @@ internal SelectMenuComponent(string customId, List options, st MaxValues = maxValues; IsDisabled = disabled; Type = type; - ChannelTypes = channelTypes?.ToArray() ?? Array.Empty(); - DefaultValues = defaultValues?.ToArray() ?? Array.Empty(); + Id = id; + ChannelTypes = channelTypes?.ToArray() ?? []; + DefaultValues = defaultValues?.ToArray() ?? []; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorComponent.cs new file mode 100644 index 0000000000..00c2e7cb9a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorComponent.cs @@ -0,0 +1,30 @@ +namespace Discord; + +/// +/// Represents a separator component. +/// +public class SeparatorComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.Separator; + + /// + public int? Id { get; } + + /// + /// Gets whether this component is a divider. + /// + public bool? IsDivider { get; } + + /// + /// Gets the spacing of this component. + /// + public SeparatorSpacingSize? Spacing { get; } + + internal SeparatorComponent(bool? isDivider, SeparatorSpacingSize? spacing, int? id = null) + { + IsDivider = isDivider; + Spacing = spacing; + Id = id; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorSpacingSize.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorSpacingSize.cs new file mode 100644 index 0000000000..0336eb747a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SeparatorSpacingSize.cs @@ -0,0 +1,17 @@ +namespace Discord; + +/// +/// Represents the spacing of a separator component. +/// +public enum SeparatorSpacingSize +{ + /// + /// The separator has a small spacing. + /// + Small = 1, + + /// + /// The separator has a large spacing. + /// + Large = 2 +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextDisplayComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextDisplayComponent.cs new file mode 100644 index 0000000000..e2aec7e858 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextDisplayComponent.cs @@ -0,0 +1,24 @@ +namespace Discord; + +/// +/// Represents a text display component. +/// +public class TextDisplayComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.TextDisplay; + + /// + public int? Id { get; } + + /// + /// Gets the content of this component. + /// + public string Content { get; } + + internal TextDisplayComponent(string content, int? id = null) + { + Id = id; + Content = content; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs index e2da1132c8..d76014bc07 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs @@ -3,7 +3,7 @@ namespace Discord /// /// Represents a text input. /// - public class TextInputComponent : IMessageComponent + public class TextInputComponent : IInteractableComponent { /// public ComponentType Type => ComponentType.TextInput; @@ -11,6 +11,9 @@ public class TextInputComponent : IMessageComponent /// public string CustomId { get; } + /// + public int? Id { get; } + /// /// Gets the label of the component; this is the text shown above it. /// @@ -47,7 +50,7 @@ public class TextInputComponent : IMessageComponent public string Value { get; } internal TextInputComponent(string customId, string label, string placeholder, int? minLength, int? maxLength, - TextInputStyle style, bool? required, string value) + TextInputStyle style, bool? required, string value, int? id) { CustomId = customId; Label = label; @@ -57,6 +60,7 @@ internal TextInputComponent(string customId, string label, string placeholder, i Style = style; Required = required; Value = value; + Id = id; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs index 9bbcf687f9..292f26b4fb 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputStyle.cs @@ -6,6 +6,7 @@ public enum TextInputStyle /// Intended for short, single-line text. /// Short = 1, + /// /// Intended for longer or multiline text. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ThumbnailComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ThumbnailComponent.cs new file mode 100644 index 0000000000..b42233923b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ThumbnailComponent.cs @@ -0,0 +1,36 @@ +namespace Discord; + +/// +/// Represents a thumbnail component. +/// +public class ThumbnailComponent : IMessageComponent +{ + /// + public ComponentType Type => ComponentType.Thumbnail; + + /// + public int? Id { get; } + + /// + /// Gets the media of the component. + /// + public UnfurledMediaItem Media { get; } + + /// + /// Gets the description of the component. + /// + public string Description { get; } + + /// + /// Gets whether the component is a spoiler. + /// + public bool IsSpoiler { get; } + + internal ThumbnailComponent(int? id, UnfurledMediaItem media, string description, bool? isSpoiler) + { + Id = id; + Media = media; + Description = description; + IsSpoiler = isSpoiler ?? false; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItem.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItem.cs new file mode 100644 index 0000000000..52177794ed --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItem.cs @@ -0,0 +1,17 @@ +namespace Discord; + +/// +/// Represents a media item that has been unfurled. +/// +public class UnfurledMediaItem +{ + /// + /// Gets the URL of this media item. + /// + public string Url { get; } + + internal UnfurledMediaItem(string url) + { + Url = url; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItemLoadingState.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItemLoadingState.cs new file mode 100644 index 0000000000..f6df157c45 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/UnfurledMediaItemLoadingState.cs @@ -0,0 +1,24 @@ +namespace Discord; + +public enum UnfurledMediaItemLoadingState +{ + /// + /// The state of the media item is unknown. + /// + Unknown = 0, + + /// + /// The media item is currently loading. + /// + Loading = 1, + + /// + /// The media item was successfully loaded. + /// + LoadingSuccess = 2, + + /// + /// The media item was not found. + /// + LoadingNotFound = 3 +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs index a435d33ef1..6062db0cc0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs @@ -1,19 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { /// /// Represents a modal interaction. /// - public class Modal : IMessageComponent + public class Modal { - /// - public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); - /// /// Gets the title of the modal. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs index c534e9f385..0b5c9bb47a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs @@ -111,21 +111,20 @@ public ModalBuilder AddComponents(List components, int row) } /// - /// Gets a by the specified . + /// Gets a by the specified . /// - /// The type of the component to get. - /// The of the component to get. + /// The type of the component to get. + /// The of the component to get. /// - /// The component of type that was found, otherwise. + /// The component of type that was found, otherwise. /// - public TMessageComponent GetComponent(string customId) - where TMessageComponent : class, IMessageComponent + public TMessageComponentBuilder GetComponent(string customId) + where TMessageComponentBuilder : class, IInteractableComponentBuilder { Preconditions.NotNull(customId, nameof(customId)); - return Components.ActionRows - ?.SelectMany(r => r.Components.OfType()) - .FirstOrDefault(c => c?.CustomId == customId); + return Components.ActionRows?.SelectMany(r => r.Components.OfType()) + .FirstOrDefault(c => c.CustomId == customId); } /// @@ -141,7 +140,7 @@ public ModalBuilder UpdateTextInput(string customId, Action up { Preconditions.NotNull(customId, nameof(customId)); - var component = GetComponent(customId) ?? throw new ArgumentException($"There is no component of type {nameof(TextInputComponent)} with the specified custom ID in this modal builder.", nameof(customId)); + var component = GetComponent(customId) ?? throw new ArgumentException($"There is no component of type {nameof(TextInputComponent)} with the specified custom ID in this modal builder.", nameof(customId)); var row = Components.ActionRows.First(r => r.Components.Contains(component)); var builder = new TextInputBuilder @@ -159,7 +158,7 @@ public ModalBuilder UpdateTextInput(string customId, Action up updateTextInput(builder); row.Components.Remove(component); - row.AddComponent(builder.Build()); + row.AddComponent(builder); return this; } @@ -179,13 +178,13 @@ public ModalBuilder UpdateTextInput(string customId, object value) /// /// Removes a component from this builder by the specified . /// - /// The of the component to remove. + /// The of the component to remove. /// The current builder. public ModalBuilder RemoveComponent(string customId) { Preconditions.NotNull(customId, nameof(customId)); - Components.ActionRows?.ForEach(r => r.Components.RemoveAll(c => c.CustomId == customId)); + Components.ActionRows?.ForEach(r => r.Components.RemoveAll(c => c is IInteractableComponentBuilder ic && ic.CustomId == customId)); return this; } @@ -226,7 +225,7 @@ public Modal Build() public class ModalComponentBuilder { /// - /// The max length of a . + /// The max length of a . /// public const int MaxCustomIdLength = 100; @@ -314,19 +313,17 @@ public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0) { Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - var builtButton = text.Build(); - if (_actionRows == null) { _actionRows = new List { - new ActionRowBuilder().AddComponent(builtButton) + new ActionRowBuilder().AddComponent(text) }; } else { if (_actionRows.Count == row) - _actionRows.Add(new ActionRowBuilder().AddComponent(builtButton)); + _actionRows.Add(new ActionRowBuilder().AddComponent(text)); else { ActionRowBuilder actionRow; @@ -338,8 +335,8 @@ public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0) _actionRows.Add(actionRow); } - if (actionRow.CanTakeComponent(builtButton)) - actionRow.AddComponent(builtButton); + if (actionRow.CanTakeComponent(text)) + actionRow.AddComponent(text); else if (row < MaxActionRowCount) WithTextInput(text, row + 1); else diff --git a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs index 57bb8ba262..c43b3f3e8a 100644 --- a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs @@ -123,5 +123,16 @@ public void Dispose() _isDisposed = true; } } + + /// + /// Gets the url formatted with attachment:// protocol. + /// + /// + /// The formatted url. + /// + public string GetAttachmentUrl() + { + return $"attachment://{FileName}"; + } } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs index 10f1aeba16..6219e8fc9c 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs @@ -57,5 +57,10 @@ public enum MessageFlags /// This message is a voice message. /// VoiceMessage = 1 << 13, + + /// + /// This message is using v2 components. + /// + ComponentsV2 = 1 << 15, } } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 83da3a66ad..372238bf77 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -401,5 +401,11 @@ public static void Options(string name, string description) } #endregion + + public static void ValidateMessageFlags(MessageFlags flags) + { + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification and not MessageFlags.ComponentsV2 and not MessageFlags.Ephemeral) + throw new ArgumentException("The only valid MessageFlags are Ephemeral, SuppressEmbeds, SuppressNotification, ComponentsV2 and None.", nameof(flags)); + } } } diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs index 62e5c3ab03..5b9b90f35a 100644 --- a/src/Discord.Net.Interactions/InteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs @@ -44,55 +44,55 @@ internal void SetContext(IInteractionContext context) protected virtual Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => Context.Interaction.DeferAsync(ephemeral, options); - /// + /// protected virtual Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null) - => Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.RespondWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.RespondWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.RespondWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.RespondWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.RespondWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.RespondWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null) - => Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.FollowupWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.FollowupWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); - /// + /// protected virtual Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); /// protected virtual Task ReplyAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index 1778b7efec..cdcc4c89d6 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -43,8 +43,8 @@ protected override Task DeferAsync(bool ephemeral = false, RequestOptions option /// A Task representing the operation of creating the interaction response. /// /// Thrown if the interaction isn't a type of . - protected override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null) - => HandleInteractionAsync(x => x.Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll)); + protected override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => HandleInteractionAsync(x => x.Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags)); /// /// Responds to the interaction with a modal. diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index e97ca71d65..4ee6311ee3 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -8,6 +8,9 @@ internal class ActionRowComponent : IMessageComponent [JsonProperty("type")] public ComponentType Type { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("components")] public IMessageComponent[] Components { get; set; } @@ -29,9 +32,10 @@ internal ActionRowComponent(Discord.ActionRowComponent c) _ => null }; }).ToArray(); + Id = c.Id ?? Optional.Unspecified; } [JsonIgnore] - string IMessageComponent.CustomId => null; + int? IMessageComponent.Id => Id.ToNullable(); } } diff --git a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs index f820fc026d..84f6e0c825 100644 --- a/src/Discord.Net.Rest/API/Common/ButtonComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ButtonComponent.cs @@ -2,11 +2,14 @@ namespace Discord.API { - internal class ButtonComponent : IMessageComponent + internal class ButtonComponent : IInteractableComponent { [JsonProperty("type")] public ComponentType Type { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("style")] public ButtonStyle Style { get; set; } @@ -39,6 +42,7 @@ public ButtonComponent(Discord.ButtonComponent c) Url = c.Url; Disabled = c.IsDisabled; SkuId = c.SkuId ?? Optional.Unspecified; + Id = c.Id ?? Optional.Unspecified; if (c.Emote != null) { @@ -62,6 +66,9 @@ public ButtonComponent(Discord.ButtonComponent c) } [JsonIgnore] - string IMessageComponent.CustomId => CustomId.GetValueOrDefault(); + string IInteractableComponent.CustomId => CustomId.GetValueOrDefault(); + + [JsonIgnore] + int? IMessageComponent.Id => Id.ToNullable(); } } diff --git a/src/Discord.Net.Rest/API/Common/ContainerComponent.cs b/src/Discord.Net.Rest/API/Common/ContainerComponent.cs new file mode 100644 index 0000000000..969906bcb0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ContainerComponent.cs @@ -0,0 +1,36 @@ +using Discord.Rest; +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API; + +internal class ContainerComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("accent_color")] + public Optional AccentColor { get; set; } + + [JsonProperty("spoiler")] + public Optional IsSpoiler { get; set; } + + [JsonProperty("components")] + public IMessageComponent[] Components { get; set; } + + public ContainerComponent() { } + + public ContainerComponent(Discord.ContainerComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + AccentColor = component.AccentColor ?? Optional.Unspecified; + IsSpoiler = component.IsSpoiler ?? Optional.Unspecified; + Components = component.Components.Select(x => x.ToModel()).ToArray(); + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/FileComponent.cs b/src/Discord.Net.Rest/API/Common/FileComponent.cs new file mode 100644 index 0000000000..81f189ae6b --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/FileComponent.cs @@ -0,0 +1,30 @@ +using Discord.Rest; +using Newtonsoft.Json; + +namespace Discord.API; + +internal class FileComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("file")] + public UnfurledMediaItem File { get; set; } + + [JsonProperty("spoiler")] + public Optional IsSpoiler { get; set; } + + public FileComponent() { } + + public FileComponent(Discord.FileComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + File = component.File.ToModel(); + IsSpoiler = component.IsSpoiler ?? Optional.Unspecified; + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs index c64920e33a..8874324677 100644 --- a/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs +++ b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs @@ -17,7 +17,7 @@ internal class ForumThreadMessage public Optional AllowedMentions { get; set; } [JsonProperty("components")] - public Optional Components { get; set; } + public Optional Components { get; set; } [JsonProperty("sticker_ids")] public Optional Stickers { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs index 0b3302dde0..2596c490da 100644 --- a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs +++ b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs @@ -21,7 +21,7 @@ internal class InteractionCallbackData public Optional Flags { get; set; } [JsonProperty("components")] - public Optional Components { get; set; } + public Optional Components { get; set; } [JsonProperty("choices")] public Optional Choices { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/MediaGalleryComponent.cs b/src/Discord.Net.Rest/API/Common/MediaGalleryComponent.cs new file mode 100644 index 0000000000..cc26cbd155 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MediaGalleryComponent.cs @@ -0,0 +1,33 @@ +using Discord.Rest; +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API; + +internal class MediaGalleryComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("items")] + public MediaGalleryItem[] Items { get; set; } + + public MediaGalleryComponent() { } + + public MediaGalleryComponent(Discord.MediaGalleryComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + Items = component.Items.Select(x => new MediaGalleryItem + { + Description = x.Description, + IsSpoiler = x.IsSpoiler, + Media = x.Media.ToModel() + }).ToArray(); + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/MediaGalleryItem.cs b/src/Discord.Net.Rest/API/Common/MediaGalleryItem.cs new file mode 100644 index 0000000000..dda02dc5b7 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MediaGalleryItem.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class MediaGalleryItem +{ + [JsonProperty("media")] + public UnfurledMediaItem Media { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("spoiler")] + public Optional IsSpoiler { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/SectionComponent.cs b/src/Discord.Net.Rest/API/Common/SectionComponent.cs new file mode 100644 index 0000000000..76ea893d8c --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SectionComponent.cs @@ -0,0 +1,32 @@ +using Discord.Rest; +using Newtonsoft.Json; +using System.Linq; + +namespace Discord.API; + +internal class SectionComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("components")] + public IMessageComponent[] Components { get; set; } + + [JsonProperty("accessory")] + public IMessageComponent Accessory { get; set; } + + public SectionComponent() { } + + public SectionComponent(Discord.SectionComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + Components = component.Components.Select(x => x.ToModel()).ToArray(); + Accessory = component.Accessory.ToModel(); + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index c7a69568cd..f605d75b84 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -3,11 +3,14 @@ namespace Discord.API { - internal class SelectMenuComponent : IMessageComponent + internal class SelectMenuComponent : IInteractableComponent { [JsonProperty("type")] public ComponentType Type { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("custom_id")] public string CustomId { get; set; } @@ -52,5 +55,8 @@ public SelectMenuComponent(Discord.SelectMenuComponent component) ChannelTypes = component.ChannelTypes.ToArray(); DefaultValues = component.DefaultValues.Select(x => new SelectMenuDefaultValue {Id = x.Id, Type = x.Type}).ToArray(); } + + [JsonIgnore] + int? IMessageComponent.Id => Id.ToNullable(); } } diff --git a/src/Discord.Net.Rest/API/Common/SeparatorComponent.cs b/src/Discord.Net.Rest/API/Common/SeparatorComponent.cs new file mode 100644 index 0000000000..f01affac2f --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/SeparatorComponent.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class SeparatorComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("divider")] + public Optional IsDivider { get; set; } + + [JsonProperty("spacing")] + public Optional Spacing { get; set; } + + public SeparatorComponent() { } + + public SeparatorComponent(Discord.SeparatorComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + IsDivider = component.IsDivider ?? Optional.Unspecified; + Spacing = component.Spacing ?? Optional.Unspecified; + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/TextDisplayComponent.cs b/src/Discord.Net.Rest/API/Common/TextDisplayComponent.cs new file mode 100644 index 0000000000..04d66e06c9 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/TextDisplayComponent.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class TextDisplayComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + + public TextDisplayComponent() { } + + public TextDisplayComponent(Discord.TextDisplayComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + Content = component.Content; + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs index a475345fcf..458cdaca56 100644 --- a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs +++ b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs @@ -2,11 +2,14 @@ namespace Discord.API { - internal class TextInputComponent : IMessageComponent + internal class TextInputComponent : IInteractableComponent { [JsonProperty("type")] public ComponentType Type { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("style")] public TextInputStyle Style { get; set; } @@ -44,6 +47,10 @@ public TextInputComponent(Discord.TextInputComponent component) MaxLength = component.MaxLength ?? Optional.Unspecified; Required = component.Required ?? Optional.Unspecified; Value = component.Value ?? Optional.Unspecified; + Id = component.Id ?? Optional.Unspecified; } + + [JsonIgnore] + int? IMessageComponent.Id => Id.ToNullable(); } } diff --git a/src/Discord.Net.Rest/API/Common/ThumbnailComponent.cs b/src/Discord.Net.Rest/API/Common/ThumbnailComponent.cs new file mode 100644 index 0000000000..bc8b40dc38 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ThumbnailComponent.cs @@ -0,0 +1,35 @@ +using Discord.Rest; +using Newtonsoft.Json; + +namespace Discord.API; + +internal class ThumbnailComponent : IMessageComponent +{ + [JsonProperty("type")] + public ComponentType Type { get; set; } + + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("media")] + public UnfurledMediaItem Media { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("spoiler")] + public Optional IsSpoiler { get; set; } + + public ThumbnailComponent() { } + + public ThumbnailComponent(Discord.ThumbnailComponent component) + { + Type = component.Type; + Id = component.Id ?? Optional.Unspecified; + Media = component.Media.ToModel(); + Description = component.Description; + IsSpoiler = component.IsSpoiler; + } + + int? IMessageComponent.Id => Id.ToNullable(); +} diff --git a/src/Discord.Net.Rest/API/Common/UnfurledMediaItem.cs b/src/Discord.Net.Rest/API/Common/UnfurledMediaItem.cs new file mode 100644 index 0000000000..558d8d4141 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/UnfurledMediaItem.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class UnfurledMediaItem +{ + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("proxy_url")] + public Optional ProxyUrl { get; set; } + + [JsonProperty("height")] + public Optional Height { get; set; } + + [JsonProperty("width")] + public Optional Width { get; set; } + + [JsonProperty("content_type")] + public Optional ContentType { get; set; } + + [JsonProperty("loading_state")] + public Optional LoadingState { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index e22f8fbd47..42174ab944 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -24,7 +24,7 @@ internal class CreateMessageParams public Optional MessageReference { get; set; } [JsonProperty("components")] - public Optional Components { get; set; } + public Optional Components { get; set; } [JsonProperty("sticker_ids")] public Optional Stickers { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs index 85cffe13f4..710e14ac42 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -24,7 +24,7 @@ internal class CreateMultipartPostAsync public Optional Content { get; set; } public Optional Embeds { get; set; } public Optional AllowedMentions { get; set; } - public Optional MessageComponent { get; set; } + public Optional MessageComponent { get; set; } public Optional Flags { get; set; } public Optional Stickers { get; set; } public Optional TagIds { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs index d74678f631..f568cb9dde 100644 --- a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -1,28 +1,22 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Discord.API.Rest +namespace Discord.API.Rest; + +internal class CreatePostParams { - internal class CreatePostParams - { - // thread - [JsonProperty("name")] - public string Title { get; set; } + // thread + [JsonProperty("name")] + public string Title { get; set; } - [JsonProperty("auto_archive_duration")] - public ThreadArchiveDuration ArchiveDuration { get; set; } + [JsonProperty("auto_archive_duration")] + public ThreadArchiveDuration ArchiveDuration { get; set; } - [JsonProperty("rate_limit_per_user")] - public Optional Slowmode { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional Slowmode { get; set; } - [JsonProperty("message")] - public ForumThreadMessage Message { get; set; } + [JsonProperty("message")] + public ForumThreadMessage Message { get; set; } - [JsonProperty("applied_tags")] - public Optional Tags { get; set; } - } + [JsonProperty("applied_tags")] + public Optional Tags { get; set; } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs index 52dd1806dc..e3b10738bf 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; namespace Discord.API.Rest @@ -37,7 +38,7 @@ internal class CreateWebhookMessageParams public Optional Flags { get; set; } [JsonProperty("components")] - public Optional Components { get; set; } + public Optional Components { get; set; } [JsonProperty("file")] public Optional File { get; set; } @@ -55,6 +56,7 @@ public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); + var extraFlags = MessageFlags.None; if (File.IsSpecified) { d["file"] = File.Value; @@ -77,14 +79,21 @@ public IReadOnlyDictionary ToDictionary() payload["embeds"] = Embeds.Value; if (AllowedMentions.IsSpecified) payload["allowed_mentions"] = AllowedMentions.Value; + + if (Components.IsSpecified) + { payload["components"] = Components.Value; + if (Components.Value.Any(x => x.Type is not ComponentType.ActionRow)) + extraFlags |= MessageFlags.ComponentsV2; + } + + payload["flags"] = Flags.GetValueOrDefault(MessageFlags.None) | extraFlags; + if (ThreadName.IsSpecified) payload["thread_name"] = ThreadName.Value; if (AppliedTags.IsSpecified) payload["applied_tags"] = AppliedTags.Value; - if (Flags.IsSpecified) - payload["flags"] = Flags.Value; if (Poll.IsSpecified) payload["poll"] = Poll.Value; diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs index a2c7cbee6b..2afc464132 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs @@ -14,7 +14,7 @@ internal class ModifyInteractionResponseParams public Optional AllowedMentions { get; set; } [JsonProperty("components")] - public Optional Components { get; set; } + public Optional Components { get; set; } [JsonProperty("flags")] public Optional Flags { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs index 3dba45a5b7..3bee06750d 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -1,19 +1,21 @@ using Newtonsoft.Json; -namespace Discord.API.Rest +namespace Discord.API.Rest; + +internal class ModifyMessageParams { - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - internal class ModifyMessageParams - { - [JsonProperty("content")] - public Optional Content { get; set; } - [JsonProperty("embeds")] - public Optional Embeds { get; set; } - [JsonProperty("components")] - public Optional Components { get; set; } - [JsonProperty("flags")] - public Optional Flags { get; set; } - [JsonProperty("allowed_mentions")] - public Optional AllowedMentions { get; set; } - } + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs index e73efaf36a..db80eba75a 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs @@ -1,17 +1,18 @@ using Newtonsoft.Json; -namespace Discord.API.Rest +namespace Discord.API.Rest; + +internal class ModifyWebhookMessageParams { - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - internal class ModifyWebhookMessageParams - { - [JsonProperty("content")] - public Optional Content { get; set; } - [JsonProperty("embeds")] - public Optional Embeds { get; set; } - [JsonProperty("allowed_mentions")] - public Optional AllowedMentions { get; set; } - [JsonProperty("components")] - public Optional Components { get; set; } - } + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } } diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index c1e4309eed..5a10353428 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -21,7 +21,7 @@ internal class UploadFileParams public Optional Embeds { get; set; } public Optional AllowedMentions { get; set; } public Optional MessageReference { get; set; } - public Optional MessageComponent { get; set; } + public Optional MessageComponent { get; set; } public Optional Flags { get; set; } public Optional Stickers { get; set; } public Optional Poll { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs index dcdc7e65a8..0497495dfa 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs @@ -21,7 +21,7 @@ internal class UploadInteractionFileParams public Optional IsTTS { get; set; } public Optional Embeds { get; set; } public Optional AllowedMentions { get; set; } - public Optional MessageComponents { get; set; } + public Optional MessageComponents { get; set; } public Optional Flags { get; set; } public Optional Poll { get; set; } @@ -44,8 +44,10 @@ public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); + var extraFlags = MessageFlags.None; + if (Files.Any(x => x.Waveform is not null && x.DurationSeconds is not null)) - Flags = Flags.GetValueOrDefault(MessageFlags.None) | MessageFlags.VoiceMessage; + extraFlags |= MessageFlags.VoiceMessage; var payload = new Dictionary(); payload["type"] = Type; @@ -55,20 +57,26 @@ public IReadOnlyDictionary ToDictionary() data["content"] = Content.Value; if (IsTTS.IsSpecified) data["tts"] = IsTTS.Value; - if (MessageComponents.IsSpecified) - data["components"] = MessageComponents.Value; if (Embeds.IsSpecified) data["embeds"] = Embeds.Value; if (AllowedMentions.IsSpecified) data["allowed_mentions"] = AllowedMentions.Value; - if (Flags.IsSpecified) - data["flags"] = Flags.Value; + + if (MessageComponents.IsSpecified) + { + data["components"] = MessageComponents.Value; + if (MessageComponents.Value.Any(x => x.Type is not ComponentType.ActionRow)) + extraFlags |= MessageFlags.ComponentsV2; + } + + data["flags"] = Flags.GetValueOrDefault(MessageFlags.None) | extraFlags; + if (Poll.IsSpecified) data["poll"] = Poll.Value; - List attachments = new(); + List attachments = []; - for (int n = 0; n != Files.Length; n++) + for (var n = 0; n != Files.Length; n++) { var attachment = Files[n]; diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index f33eecfb9d..4da38e9dab 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -22,7 +22,7 @@ internal class UploadWebhookFileParams public Optional AvatarUrl { get; set; } public Optional Embeds { get; set; } public Optional AllowedMentions { get; set; } - public Optional MessageComponents { get; set; } + public Optional MessageComponents { get; set; } public Optional Flags { get; set; } public Optional ThreadName { get; set; } public Optional AppliedTags { get; set; } @@ -37,8 +37,10 @@ public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); + var extraFlags = MessageFlags.None; + if (Files.Any(x => x.Waveform is not null && x.DurationSeconds is not null)) - Flags = Flags.GetValueOrDefault(MessageFlags.None) | MessageFlags.VoiceMessage; + extraFlags |= MessageFlags.VoiceMessage; var payload = new Dictionary(); if (Content.IsSpecified) @@ -51,14 +53,20 @@ public IReadOnlyDictionary ToDictionary() payload["username"] = Username.Value; if (AvatarUrl.IsSpecified) payload["avatar_url"] = AvatarUrl.Value; - if (MessageComponents.IsSpecified) - payload["components"] = MessageComponents.Value; if (Embeds.IsSpecified) payload["embeds"] = Embeds.Value; if (AllowedMentions.IsSpecified) payload["allowed_mentions"] = AllowedMentions.Value; - if (Flags.IsSpecified) - payload["flags"] = Flags.Value; + + if (MessageComponents.IsSpecified) + { + payload["components"] = MessageComponents.Value; + if (MessageComponents.Value.Any(x => x.Type is not ComponentType.ActionRow)) + extraFlags |= MessageFlags.ComponentsV2; + } + + payload["flags"] = Flags.GetValueOrDefault(MessageFlags.None) | extraFlags; + if (ThreadName.IsSpecified) payload["thread_name"] = ThreadName.Value; if (AppliedTags.IsSpecified) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 91d533a596..765b4cd2ce 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -2779,6 +2779,8 @@ private static string WebhookQuery(bool wait = false, ulong? threadId = null) if (threadId.HasValue) querys.Add($"thread_id={threadId}"); + querys.Add("with_components=true"); + return $"{string.Join("&", querys)}"; } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 3fd65d02d6..9229bc73f5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -308,10 +308,10 @@ public static async Task SendMessageAsync(IMessageChannel chann Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; - + Preconditions.ValidateMessageFlags(flags); var args = new CreateMessageParams { @@ -320,7 +320,7 @@ public static async Task SendMessageAsync(IMessageChannel chann Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified @@ -434,8 +434,10 @@ public static async Task SendFilesAsync(IMessageChannel channel } } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + + Preconditions.ValidateMessageFlags(flags); if (stickers != null) { @@ -449,7 +451,7 @@ public static async Task SendFilesAsync(IMessageChannel channel Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, - MessageComponent = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponent = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index a133211d8e..29be16cb47 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -163,7 +163,7 @@ public static async Task CreatePostAsync(IForumChannel channe Content = text, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, Flags = flags, - Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Components = components?.Components?.Any() ?? false ? components.Components.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, }, Tags = tagIds @@ -224,7 +224,7 @@ public static async Task CreatePostAsync(IForumChannel channe Content = text, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, Flags = flags, - MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, Slowmode = slowmode, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, Title = title, diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index dae9b890d7..ef16c0f55c 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -1,12 +1,11 @@ using Discord.API.Rest; -using Discord.Net.Rest; + using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using DataModel = Discord.API.ApplicationCommandInteractionData; + using Model = Discord.API.Interaction; namespace Discord.Rest @@ -81,7 +80,8 @@ public override string Respond( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -89,7 +89,7 @@ public override string Respond( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -123,8 +123,12 @@ public override string Respond( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, Poll = poll?.ToModel() ?? Optional.Unspecified } }; @@ -152,12 +156,13 @@ public override Task FollowupAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -172,13 +177,15 @@ public override Task FollowupAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } @@ -194,7 +201,8 @@ public override async Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -203,7 +211,7 @@ public override async Task FollowupWithFileAsync( Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); using (var file = new FileAttachment(fileStream, fileName)) - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } /// @@ -218,7 +226,8 @@ public override async Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); @@ -226,7 +235,7 @@ public override async Task FollowupWithFileAsync( Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); using (var file = new FileAttachment(File.OpenRead(filePath), fileName)) - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } /// @@ -240,9 +249,10 @@ public override Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { - return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + return FollowupWithFilesAsync([attachment], text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); } /// @@ -256,12 +266,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -289,23 +300,22 @@ public override Task FollowupWithFilesAsync( { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } - } - - var flags = MessageFlags.None; - - if (ephemeral) - flags |= MessageFlags.Ephemeral; + }; var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) - { - Flags = flags, - Content = text, - IsTTS = isTTS, - Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, - AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified - }; + { + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, + Content = text, + IsTTS = isTTS, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified + }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 6afbf74e98..ce07a2940b 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -430,8 +430,8 @@ public static Task DeleteUnknownApplicationCommandAsync(BaseDiscordClient client Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, }; return client.ApiClient.ModifyInteractionFollowupMessageAsync(apiArgs, message.Id, message.Token, options); @@ -478,8 +478,7 @@ public static Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFoll Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] : Optional.Unspecified, Flags = args.Flags }; @@ -495,8 +494,7 @@ public static Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFoll Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, MessageComponents = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] : Optional.Unspecified }; return client.ApiClient.ModifyInteractionResponseAsync(apiArgs, token, options); diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 811b45605e..8748ef4a37 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -77,7 +77,8 @@ public override string Respond( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -85,7 +86,7 @@ public override string Respond( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -119,14 +120,16 @@ public override string Respond( AllowedMentions = allowedMentions?.ToModel(), Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, } }; - if (ephemeral) - response.Data.Value.Flags = MessageFlags.Ephemeral; - lock (_lock) { if (HasResponded) @@ -208,8 +211,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified } }; @@ -227,8 +230,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, MessageComponents = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified }; @@ -256,12 +259,13 @@ public override Task FollowupAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -276,14 +280,15 @@ public override Task FollowupAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : MessageFlags.None, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, Poll = poll?.ToModel() ?? Optional.Unspecified }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } @@ -299,7 +304,8 @@ public override async Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -308,7 +314,7 @@ public override async Task FollowupWithFileAsync( Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); using (var file = new FileAttachment(fileStream, fileName)) - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } /// @@ -323,7 +329,8 @@ public override async Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { Preconditions.NotNullOrEmpty(filePath, nameof(filePath), "Path must exist"); @@ -331,7 +338,7 @@ public override async Task FollowupWithFileAsync( Preconditions.NotNullOrEmpty(fileName, nameof(fileName), "File Name must not be empty or null"); using (var file = new FileAttachment(File.OpenRead(filePath), fileName)) - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } /// @@ -345,9 +352,10 @@ public override Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { - return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + return FollowupWithFilesAsync([attachment], text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); } /// @@ -361,12 +369,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -396,19 +405,18 @@ public override Task FollowupWithFilesAsync( } } - var flags = MessageFlags.None; - - if (ephemeral) - flags |= MessageFlags.Ephemeral; - var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { - Flags = flags, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs index bc44d0df55..f72eceecb4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs @@ -93,7 +93,7 @@ internal RestMessageComponentData(Model model, BaseDiscordClient discord, IGuild } } - internal RestMessageComponentData(IMessageComponent component, BaseDiscordClient discord, IGuild guild) + internal RestMessageComponentData(IInteractableComponent component, BaseDiscordClient discord, IGuild guild) { CustomId = component.CustomId; Type = component.Type; diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index edeeea0b90..e7d08e4192 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; @@ -137,12 +138,13 @@ public override Task FollowupAsync( MessageComponent component = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -157,13 +159,15 @@ public override Task FollowupAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ??Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Components = component?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } @@ -194,12 +198,13 @@ public override Task FollowupWithFileAsync( MessageComponent component = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -216,14 +221,16 @@ public override Task FollowupWithFileAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Components = component?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, File = fileStream is not null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } @@ -254,12 +261,13 @@ public override async Task FollowupWithFileAsync( MessageComponent component = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -279,14 +287,16 @@ public override async Task FollowupWithFileAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Components = component?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, File = fileStream != null ? new MultipartFile(fileStream, fileName) : Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); } @@ -315,7 +325,8 @@ public override string Respond( MessageComponent component = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -323,7 +334,7 @@ public override string Respond( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -357,8 +368,12 @@ public override string Respond( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + Components = component?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, Poll = poll?.ToModel() ?? Optional.Unspecified } }; @@ -390,12 +405,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -425,19 +441,18 @@ public override Task FollowupWithFilesAsync( } } - var flags = MessageFlags.None; - - if (ephemeral) - flags |= MessageFlags.Ephemeral; - var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { - Flags = flags, + Flags = ephemeral + ? flags | MessageFlags.Ephemeral + : flags == MessageFlags.None + ? Optional.Unspecified + : flags, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); @@ -454,9 +469,10 @@ public override Task FollowupWithFileAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { - return FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + return FollowupWithFilesAsync([attachment], text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); } /// @@ -541,8 +557,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified } }; @@ -551,7 +567,7 @@ public async Task UpdateAsync(Action func, RequestOptions opt } else { - var attachments = args.Attachments.Value?.ToArray() ?? Array.Empty(); + var attachments = args.Attachments.Value?.ToArray() ?? []; var response = new API.Rest.UploadInteractionFileParams(attachments) { @@ -560,8 +576,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, MessageComponents = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified }; diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs index c1f329e7d3..1831d4b51a 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; using System.Linq; -using DataModel = Discord.API.MessageComponentInteractionData; -using InterationModel = Discord.API.Interaction; using Model = Discord.API.ModalInteractionData; namespace Discord.Rest @@ -26,7 +23,7 @@ internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild) { CustomId = model.CustomId; Components = model.Components - .SelectMany(x => x.Components) + .SelectMany(x => x.Components.OfType()) .Select(x => new RestMessageComponentData(x, discord, guild)) .ToArray(); } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 551eb9abb8..d8713ccccb 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -337,7 +337,7 @@ public async Task ModifyOriginalResponseAsync(Action public abstract string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -356,7 +356,7 @@ public abstract string Respond(string text = null, Embed[] embeds = null, bool i /// contains the sent message. /// public abstract Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -377,7 +377,7 @@ public abstract Task FollowupAsync(string text = null, Embe /// contains the sent message. /// public abstract Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -398,7 +398,7 @@ public abstract Task FollowupWithFileAsync(Stream fileStrea /// contains the sent message. /// public abstract Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -418,7 +418,7 @@ public abstract Task FollowupWithFileAsync(string filePath, /// contains the sent message. /// public abstract Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -438,7 +438,7 @@ public abstract Task FollowupWithFileAsync(FileAttachment a /// contains the sent message. /// public abstract Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// public Task DeleteOriginalResponseAsync(RequestOptions options = null) @@ -457,7 +457,7 @@ public Task RespondWithPremiumRequiredAsync(RequestOptions options = null) /// Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, - MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) + MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) => Task.FromResult(Respond(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll)); /// Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options) @@ -465,45 +465,48 @@ Task IDiscordInteraction.DeferAsync(bool ephemeral, RequestOptions options) /// Task IDiscordInteraction.RespondWithModalAsync(Modal modal, RequestOptions options) => Task.FromResult(RespondWithModal(modal, options)); + /// async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, - MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.GetOriginalResponseAsync(RequestOptions options) => await GetOriginalResponseAsync(options).ConfigureAwait(false); /// async Task IDiscordInteraction.ModifyOriginalResponseAsync(Action func, RequestOptions options) => await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false); + /// async Task IDiscordInteraction.FollowupWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, - AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, - AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(filePath, text, fileName, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(filePath, text, fileName, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, - bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, - bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); + /// Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, - AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) => throw new NotSupportedException("REST-Based interactions don't support files."); + AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) => throw new NotSupportedException("REST-Based interactions don't support files."); #if NETCOREAPP3_0_OR_GREATER != true /// Task IDiscordInteraction.RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, - AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) => throw new NotSupportedException("REST-Based interactions don't support files."); + AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) => throw new NotSupportedException("REST-Based interactions don't support files."); /// Task IDiscordInteraction.RespondWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, - bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) => throw new NotSupportedException("REST-Based interactions don't support files."); + bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) => throw new NotSupportedException("REST-Based interactions don't support files."); /// Task IDiscordInteraction.RespondWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, - AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) => throw new NotSupportedException("REST-Based interactions don't support files."); + AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) => throw new NotSupportedException("REST-Based interactions don't support files."); #endif #endregion } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs index 82b44c25ba..5b93127503 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -37,11 +37,11 @@ public string AcknowledgePing() public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException(); public override string RespondWithModal(Modal modal, RequestOptions options = null) => throw new NotSupportedException(); - public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); - public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); - public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); - public override Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); - public override Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); - public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) => throw new NotSupportedException(); + public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); + public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); + public override Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); + public override Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); + public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs index dcad1dbd43..6c60990b70 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -100,17 +100,17 @@ public string Respond(RequestOptions options = null, params AutocompleteResult[] => Respond(result, options); public override string Defer(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override string Respond(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override string RespondWithModal(Modal modal, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index ac1eeb1132..f7b26d0923 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -55,16 +55,16 @@ public static Task ModifyAsync(ulong channelId, ulong msgId, BaseDiscordC Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); // check that user flag and user Id list are exclusive, same with role flag and role Id list - if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + if (allowedMentions is { AllowedTypes: not null }) { if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && - allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + allowedMentions.UserIds is { Count: > 0 }) { throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); } if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && - allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + allowedMentions.RoleIds is { Count: > 0 }) { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } @@ -93,13 +93,13 @@ public static Task ModifyAsync(ulong channelId, ulong msgId, BaseDiscordC Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), - Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified, + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] : Optional.Unspecified, }; return client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options); } else { - var attachments = args.Attachments.Value?.ToArray() ?? Array.Empty(); + var attachments = args.Attachments.Value?.ToArray() ?? []; var apiArgs = new UploadFileParams(attachments) { @@ -107,7 +107,7 @@ public static Task ModifyAsync(ulong channelId, ulong msgId, BaseDiscordC Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), - MessageComponent = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified + MessageComponent = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] : Optional.Unspecified }; return client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 132f1c97d7..c418c4855f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -95,12 +95,12 @@ public abstract class RestMessage : RestEntity, IMessage, IUpdateable /// public PurchaseNotification PurchaseNotification { get; private set; } - + /// public MessageCallData? CallData { get; private set; } /// - public IReadOnlyCollection Components { get; private set; } + public IReadOnlyCollection Components { get; private set; } /// /// Gets a collection of the mentioned users in the message. /// @@ -150,7 +150,7 @@ internal virtual void Update(Model model) if (model.Activity.IsSpecified) { // create a new Activity from the API model - Activity = new MessageActivity() + Activity = new MessageActivity { Type = model.Activity.Value.Type.Value, PartyId = model.Activity.Value.PartyId.GetValueOrDefault() @@ -170,62 +170,9 @@ internal virtual void Update(Model model) }; } - if (model.Components.IsSpecified) - { - Components = model.Components.Value.Where(x => x.Type is ComponentType.ActionRow) - .Select(x => new ActionRowComponent(((API.ActionRowComponent)x).Components.Select(y => - { - switch (y.Type) - { - case ComponentType.Button: - { - var parsed = (API.ButtonComponent)y; - return new Discord.ButtonComponent( - parsed.Style, - parsed.Label.GetValueOrDefault(), - parsed.Emote.IsSpecified - ? parsed.Emote.Value.Id.HasValue - ? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault()) - : new Emoji(parsed.Emote.Value.Name) - : null, - parsed.CustomId.GetValueOrDefault(), - parsed.Url.GetValueOrDefault(), - parsed.Disabled.GetValueOrDefault(), - parsed.SkuId.ToNullable()); - } - case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: - { - var parsed = (API.SelectMenuComponent)y; - return new SelectMenuComponent( - parsed.CustomId, - parsed.Options?.Select(z => new SelectMenuOption( - z.Label, - z.Value, - z.Description.GetValueOrDefault(), - z.Emoji.IsSpecified - ? z.Emoji.Value.Id.HasValue - ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) - : new Emoji(z.Emoji.Value.Name) - : null, - z.Default.ToNullable())).ToList(), - parsed.Placeholder.GetValueOrDefault(), - parsed.MinValues, - parsed.MaxValues, - parsed.Disabled, - parsed.Type, - parsed.ChannelTypes.GetValueOrDefault(), - parsed.DefaultValues.IsSpecified - ? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type)) - : Array.Empty() - ); - } - default: - return null; - } - }).ToList())).ToImmutableArray(); - } - else - Components = new List(); + Components = model.Components.IsSpecified + ? model.Components.Value.Select(x => x.ToEntity()).ToImmutableArray() + : []; if (model.Flags.IsSpecified) Flags = model.Flags.Value; @@ -236,15 +183,16 @@ internal virtual void Update(Model model) if (value.Length > 0) { var reactions = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - reactions.Add(RestReaction.Create(value[i])); + foreach (var t in value) + reactions.Add(RestReaction.Create(t)); + _reactions = reactions.ToImmutable(); } else - _reactions = ImmutableArray.Create(); + _reactions = []; } else - _reactions = ImmutableArray.Create(); + _reactions = []; if (model.Interaction.IsSpecified) { @@ -289,11 +237,11 @@ internal virtual void Update(Model model) ? new GuildProductPurchase(model.PurchaseNotification.Value.ProductPurchase.Value.ListingId, model.PurchaseNotification.Value.ProductPurchase.Value.ProductName) : null); } - + if (model.Call.IsSpecified) CallData = new MessageCallData(model.Call.Value.Participants, model.Call.Value.EndedTimestamp.ToNullable()); } - + /// public async Task UpdateAsync(RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 4f80fa4385..fb5a8985d7 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -84,7 +84,7 @@ internal override void Update(Model model) if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; if (model.RoleMentions.IsSpecified) - _roleMentionIds = model.RoleMentions.Value.ToImmutableArray(); + _roleMentionIds = [..model.RoleMentions.Value]; if (model.Attachments.IsSpecified) { @@ -92,12 +92,13 @@ internal override void Update(Model model) if (value.Length > 0) { var attachments = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - attachments.Add(Attachment.Create(value[i], Discord)); + foreach (var t in value) + attachments.Add(Attachment.Create(t, Discord)); + _attachments = attachments.ToImmutable(); } else - _attachments = ImmutableArray.Create(); + _attachments = []; } if (model.Embeds.IsSpecified) @@ -106,12 +107,13 @@ internal override void Update(Model model) if (value.Length > 0) { var embeds = ImmutableArray.CreateBuilder(value.Length); - for (int i = 0; i < value.Length; i++) - embeds.Add(value[i].ToEntity()); + foreach (var t in value) + embeds.Add(t.ToEntity()); + _embeds = embeds.ToImmutable(); } else - _embeds = ImmutableArray.Create(); + _embeds = []; } var guildId = (Channel as IGuildChannel)?.GuildId; @@ -123,7 +125,7 @@ internal override void Update(Model model) model.Content = text; } - if (model.ReferencedMessage.IsSpecified && model.ReferencedMessage.Value != null) + if (model.ReferencedMessage is { IsSpecified: true, Value: not null }) { var refMsg = model.ReferencedMessage.Value; IUser refMsgAuthor = MessageHelper.GetAuthor(Discord, guild, refMsg.Author.Value, refMsg.WebhookId.ToNullable()); @@ -141,7 +143,7 @@ internal override void Update(Model model) _stickers = stickers.ToImmutable(); } else - _stickers = ImmutableArray.Create(); + _stickers = []; } if (model.Resolved.IsSpecified) diff --git a/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs b/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs new file mode 100644 index 0000000000..881b1f6c00 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs @@ -0,0 +1,198 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Rest; + +internal static class MessageComponentExtension +{ + internal static IMessageComponent ToModel(this IMessageComponent component) + { + switch (component) + { + case ActionRowComponent actionRow: + return new API.ActionRowComponent(actionRow); + + case ButtonComponent btn: + return new API.ButtonComponent(btn); + + case SelectMenuComponent select: + return new API.SelectMenuComponent(select); + + case TextInputComponent textInput: + return new API.TextInputComponent(textInput); + + case TextDisplayComponent textDisplay: + return new API.TextDisplayComponent(textDisplay); + + case SectionComponent section: + return new API.SectionComponent(section); + + case ThumbnailComponent thumbnail: + return new API.ThumbnailComponent(thumbnail); + + case MediaGalleryComponent mediaGallery: + return new API.MediaGalleryComponent(mediaGallery); + + case SeparatorComponent separator: + return new API.SeparatorComponent(separator); + + case FileComponent file: + return new API.FileComponent(file); + + case ContainerComponent container: + return new API.ContainerComponent(container); + } + + return null; + } + + internal static IMessageComponent ToEntity(this IMessageComponent component) + { + switch (component.Type) + { + case ComponentType.ActionRow: + { + var parsed = (API.ActionRowComponent)component; + return new ActionRowComponent + { + Id = component.Id, + Components = parsed.Components.Select(x => x.ToEntity()).ToImmutableArray() + }; + } + + case ComponentType.Button: + { + var parsed = (API.ButtonComponent)component; + return new ButtonComponent( + parsed.Style, + parsed.Label.GetValueOrDefault(), + parsed.Emote.IsSpecified + ? parsed.Emote.Value.Id.HasValue + ? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault()) + : new Emoji(parsed.Emote.Value.Name) + : null, + parsed.CustomId.GetValueOrDefault(), + parsed.Url.GetValueOrDefault(), + parsed.Disabled.GetValueOrDefault(), + parsed.SkuId.ToNullable(), + parsed.Id.ToNullable()); + } + + case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: + { + var parsed = (API.SelectMenuComponent)component; + return new SelectMenuComponent( + parsed.CustomId, + parsed.Options?.Select(z => new SelectMenuOption( + z.Label, + z.Value, + z.Description.GetValueOrDefault(), + z.Emoji.IsSpecified + ? z.Emoji.Value.Id.HasValue + ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) + : new Emoji(z.Emoji.Value.Name) + : null, + z.Default.ToNullable())).ToList(), + parsed.Placeholder.GetValueOrDefault(), + parsed.MinValues, + parsed.MaxValues, + parsed.Disabled, + parsed.Type, + parsed.Id.ToNullable(), + parsed.ChannelTypes.GetValueOrDefault(), + parsed.DefaultValues.IsSpecified + ? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type)) + : [] + ); + } + + case ComponentType.TextInput: + { + var parsed = (API.TextInputComponent)component; + return new TextInputComponent(parsed.CustomId, + parsed.Label, + parsed.Placeholder.GetValueOrDefault(null), + parsed.MinLength.ToNullable(), + parsed.MaxLength.ToNullable(), + parsed.Style, + parsed.Required.ToNullable(), + parsed.Value.GetValueOrDefault(null), + parsed.Id.ToNullable()); + } + + case ComponentType.TextDisplay: + { + var parsed = (API.TextDisplayComponent)component; + return new TextDisplayComponent(parsed.Content, parsed.Id.ToNullable()); + } + + case ComponentType.Section: + { + var parsed = (API.SectionComponent)component; + return new SectionComponent(parsed.Id.ToNullable(), + parsed.Components.Select(x => x.ToEntity()).ToImmutableArray(), + parsed.Accessory.ToEntity()); + } + + case ComponentType.Thumbnail: + { + var parsed = (API.ThumbnailComponent)component; + return new ThumbnailComponent(parsed.Id.ToNullable(), + parsed.Media.ToEntity(), + parsed.Description.GetValueOrDefault(null), + parsed.IsSpoiler.ToNullable()); + } + + case ComponentType.MediaGallery: + { + var parsed = (API.MediaGalleryComponent)component; + + return new MediaGalleryComponent( + parsed.Items.Select(x => new MediaGalleryItem(x.Media.ToEntity(), x.Description.GetValueOrDefault(null), x.IsSpoiler.GetValueOrDefault(false))).ToList(), + parsed.Id.ToNullable()); + } + + case ComponentType.Separator: + { + var parsed = (API.SeparatorComponent)component; + return new SeparatorComponent(parsed.IsDivider.ToNullable(), parsed.Spacing.ToNullable(), parsed.Id.ToNullable()); + } + + case ComponentType.File: + { + var parsed = (API.FileComponent)component; + return new FileComponent(parsed.File.ToEntity(), parsed.IsSpoiler.ToNullable(), parsed.Id.ToNullable()); + } + + case ComponentType.Container: + { + var parsed = (API.ContainerComponent)component; + return new ContainerComponent(parsed.Components.Select(x => x.ToEntity()).ToImmutableArray(), + parsed.AccentColor.GetValueOrDefault(null), + parsed.IsSpoiler.ToNullable(), + parsed.Id.ToNullable()); + } + + default: + return null; + } + } + + internal static UnfurledMediaItem ToEntity(this API.UnfurledMediaItem mediaItem) + { + return new ResolvedUnfurledMediaItem(mediaItem.Url, + mediaItem.ProxyUrl.Value, + mediaItem.Height.Value, + mediaItem.Width.Value, + mediaItem.ContentType.Value, + mediaItem.LoadingState.Value); + } + + internal static API.UnfurledMediaItem ToModel(this UnfurledMediaItem mediaItem) + { + return new API.UnfurledMediaItem + { + Url = mediaItem.Url + }; + } +} diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs index 7888219bcb..0f229f33ef 100644 --- a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -1,12 +1,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.ComponentModel; namespace Discord.Net.Converters { internal class MessageComponentConverter : JsonConverter { - public static MessageComponentConverter Instance => new MessageComponentConverter(); + public static MessageComponentConverter Instance => new (); public override bool CanRead => true; public override bool CanWrite => false; @@ -39,6 +40,27 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist case ComponentType.TextInput: messageComponent = new API.TextInputComponent(); break; + case ComponentType.TextDisplay: + messageComponent = new API.TextDisplayComponent(); + break; + case ComponentType.Thumbnail: + messageComponent = new API.ThumbnailComponent(); + break; + case ComponentType.Section: + messageComponent = new API.SectionComponent(); + break; + case ComponentType.MediaGallery: + messageComponent = new API.MediaGalleryComponent(); + break; + case ComponentType.Separator: + messageComponent = new API.SeparatorComponent(); + break; + case ComponentType.File: + messageComponent = new API.FileComponent(); + break; + case ComponentType.Container: + messageComponent = new API.ContainerComponent(); + break; } serializer.Populate(jsonObject.CreateReader(), messageComponent); return messageComponent; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index f24a4be6fd..984c898454 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -82,7 +82,8 @@ public override async Task RespondWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -90,7 +91,7 @@ public override async Task RespondWithFilesAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -115,6 +116,13 @@ public override async Task RespondWithFilesAsync( } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var response = new API.Rest.UploadInteractionFileParams(attachments?.ToArray()) { Type = InteractionResponseType.ChannelMessageWithSource, @@ -122,8 +130,8 @@ public override async Task RespondWithFilesAsync( AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, IsTTS = isTTS, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified }; @@ -149,7 +157,8 @@ public override async Task RespondAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -157,7 +166,7 @@ public override async Task RespondAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -167,21 +176,28 @@ public override async Task RespondAsync( Preconditions.ValidatePoll(poll); // check that user flag and user Id list are exclusive, same with role flag and role Id list - if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + if (allowedMentions is { AllowedTypes: not null }) { if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && - allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + allowedMentions.UserIds is { Count: > 0 }) { throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); } if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && - allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + allowedMentions.RoleIds is { Count: > 0 }) { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var response = new API.InteractionResponse { Type = InteractionResponseType.ChannelMessageWithSource, @@ -191,8 +207,8 @@ public override async Task RespondAsync( AllowedMentions = allowedMentions?.ToModel(), Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = flags, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified } }; @@ -250,13 +266,13 @@ public async Task UpdateAsync(Action func, RequestOptions opt { var allowedMentions = args.AllowedMentions.Value; if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) - && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + && allowedMentions.UserIds is { Count: > 0 }) { throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); } if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) - && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + && allowedMentions.RoleIds is { Count: > 0 }) { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); } @@ -273,8 +289,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified } }; @@ -283,7 +299,7 @@ public async Task UpdateAsync(Action func, RequestOptions opt } else { - var attachments = args.Attachments.Value?.ToArray() ?? Array.Empty(); + var attachments = args.Attachments.Value?.ToArray() ?? []; var response = new API.Rest.UploadInteractionFileParams(attachments) { @@ -292,8 +308,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, MessageComponents = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified }; @@ -321,12 +337,13 @@ public override Task FollowupAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -335,19 +352,25 @@ public override Task FollowupAsync( Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); Preconditions.ValidatePoll(poll); + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + + var args = new API.Rest.CreateWebhookMessageParams { Content = text, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); } @@ -362,12 +385,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -385,23 +409,24 @@ public override Task FollowupWithFilesAsync( if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) { if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && - allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + allowedMentions.UserIds is { Count: > 0 }) { throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); } if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && - allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + allowedMentions.RoleIds is { Count: > 0 }) { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } } - - var flags = MessageFlags.None; - + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; if (ephemeral) flags |= MessageFlags.Ephemeral; + Preconditions.ValidateMessageFlags(flags); + var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { Flags = flags, @@ -409,7 +434,7 @@ public override Task FollowupWithFilesAsync( IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs index b193b77f38..718cc3b130 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs @@ -90,13 +90,13 @@ internal SocketMessageComponentData(Model model, DiscordSocketClient discord, Cl } } - internal SocketMessageComponentData(IMessageComponent component, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) + internal SocketMessageComponentData(IInteractableComponent component, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = component.CustomId; Type = component.Type; Value = component.Type == ComponentType.TextInput - ? (component as API.TextInputComponent).Value.Value + ? ((TextInputComponent)component).Value : null; if (component is API.SelectMenuComponent select) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index 7b1b0a6502..a6de28d332 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -79,7 +79,8 @@ public override async Task RespondWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -87,7 +88,7 @@ public override async Task RespondWithFilesAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -111,6 +112,12 @@ public override async Task RespondWithFilesAsync( throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); var response = new API.Rest.UploadInteractionFileParams(attachments?.ToArray()) { @@ -119,8 +126,8 @@ public override async Task RespondWithFilesAsync( AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, IsTTS = isTTS, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified }; @@ -136,7 +143,7 @@ public override async Task RespondWithFilesAsync( HasResponded = true; } - /// + /// public override async Task RespondAsync( string text = null, Embed[] embeds = null, @@ -146,7 +153,8 @@ public override async Task RespondAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -154,7 +162,7 @@ public override async Task RespondAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -164,21 +172,28 @@ public override async Task RespondAsync( Preconditions.ValidatePoll(poll); // check that user flag and user Id list are exclusive, same with role flag and role Id list - if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + if (allowedMentions is { AllowedTypes: not null }) { if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && - allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + allowedMentions.UserIds is { Count: > 0 }) { throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); } if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && - allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + allowedMentions.RoleIds is { Count: > 0 }) { throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var response = new API.InteractionResponse { Type = InteractionResponseType.ChannelMessageWithSource, @@ -188,8 +203,8 @@ public override async Task RespondAsync( AllowedMentions = allowedMentions?.ToModel(), Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = flags, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified } }; @@ -270,9 +285,11 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, Components = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, - Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, + Flags = args.Flags.IsSpecified + ? args.Flags.Value ?? Optional.Unspecified + : Optional.Unspecified } }; @@ -289,8 +306,8 @@ public async Task UpdateAsync(Action func, RequestOptions opt AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, MessageComponents = args.Components.IsSpecified - ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() - : Optional.Unspecified, + ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() ?? [] + : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified }; @@ -318,12 +335,13 @@ public override Task FollowupAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -332,20 +350,24 @@ public override Task FollowupAsync( Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); Preconditions.ValidatePoll(poll); + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var args = new API.Rest.CreateWebhookMessageParams { Content = text, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); } @@ -360,12 +382,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -395,11 +418,13 @@ public override Task FollowupWithFilesAsync( } } - var flags = MessageFlags.None; - + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; if (ephemeral) flags |= MessageFlags.Ephemeral; + Preconditions.ValidateMessageFlags(flags); + var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { Flags = flags, @@ -407,7 +432,7 @@ public override Task FollowupWithFilesAsync( IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs index 52c7615b2d..c20562ecc7 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; using System.Linq; -using DataModel = Discord.API.MessageComponentInteractionData; -using InterationModel = Discord.API.Interaction; using Model = Discord.API.ModalInteractionData; namespace Discord.WebSocket @@ -26,7 +23,7 @@ internal SocketModalData(Model model, DiscordSocketClient discord, ClientState s { CustomId = model.CustomId; Components = model.Components - .SelectMany(x => x.Components) + .SelectMany(x => x.Components.OfType()) .Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser)) .ToArray(); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs index 7e6b484ab9..c4521513ff 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketAutocompleteInteraction.cs @@ -91,15 +91,15 @@ public async Task RespondAsync(IEnumerable result, RequestOp /// public Task RespondAsync(RequestOptions options = null, params AutocompleteResult[] result) => RespondAsync(result, options); - public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); - public override Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + public override Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) => throw new NotSupportedException("Autocomplete interactions don't support this method!"); /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 3b6da47742..075e4e31fa 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -77,7 +77,8 @@ public override async Task RespondAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -85,7 +86,7 @@ public override async Task RespondAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -110,6 +111,13 @@ public override async Task RespondAsync( } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var response = new API.InteractionResponse { Type = InteractionResponseType.ChannelMessageWithSource, @@ -119,8 +127,8 @@ public override async Task RespondAsync( AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, Embeds = embeds.Select(x => x.ToModel()).ToArray(), TTS = isTTS, - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Flags = flags, Poll = poll?.ToModel() ?? Optional.Unspecified } }; @@ -183,7 +191,8 @@ public override async Task RespondWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); @@ -191,7 +200,7 @@ public override async Task RespondWithFilesAsync( if (!InteractionHelper.CanSendResponse(this) && Discord.ResponseInternalTimeCheck) throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -216,6 +225,13 @@ public override async Task RespondWithFilesAsync( } } + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var response = new API.Rest.UploadInteractionFileParams(attachments?.ToArray()) { Type = InteractionResponseType.ChannelMessageWithSource, @@ -223,8 +239,8 @@ public override async Task RespondWithFilesAsync( AllowedMentions = allowedMentions != null ? allowedMentions?.ToModel() : Optional.Unspecified, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, IsTTS = isTTS, - Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = flags, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; @@ -250,12 +266,13 @@ public override Task FollowupAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -264,19 +281,24 @@ public override Task FollowupAsync( Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); Preconditions.ValidatePoll(poll); + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + if (ephemeral) + flags |= MessageFlags.Ephemeral; + + Preconditions.ValidateMessageFlags(flags); + var args = new API.Rest.CreateWebhookMessageParams { Content = text ?? Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsTTS = isTTS, Embeds = embeds.Select(x => x.ToModel()).ToArray(), - Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Poll = poll?.ToModel() ?? Optional.Unspecified + Components = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Poll = poll?.ToModel() ?? Optional.Unspecified, + Flags = flags, }; - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - return InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); } @@ -291,12 +313,13 @@ public override Task FollowupWithFilesAsync( MessageComponent components = null, Embed embed = null, RequestOptions options = null, - PollProperties poll = null) + PollProperties poll = null, + MessageFlags flags = MessageFlags.None) { if (!IsValidToken) throw new InvalidOperationException("Interaction token is no longer valid"); - embeds ??= Array.Empty(); + embeds ??= []; if (embed != null) embeds = new[] { embed }.Concat(embeds).ToArray(); @@ -326,11 +349,13 @@ public override Task FollowupWithFilesAsync( } } - var flags = MessageFlags.None; - + if (components?.Components?.Any(x => x.Type != ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; if (ephemeral) flags |= MessageFlags.Ephemeral; + Preconditions.ValidateMessageFlags(flags); + var args = new API.Rest.UploadWebhookFileParams(attachments.ToArray()) { Flags = flags, @@ -338,7 +363,7 @@ public override Task FollowupWithFilesAsync( IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Poll = poll?.ToModel() ?? Optional.Unspecified }; return InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 24aecb47f1..5b3c9bc054 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -211,7 +211,7 @@ ChannelType.Media or /// Message content is too long, length must be less or equal to . /// The parameters provided were invalid or the token was invalid. public abstract Task RespondAsync(string text = null, Embed[] embeds = null, bool isTTS = false, - bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + bool ephemeral = false, AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Responds to this interaction with a file attachment. @@ -232,11 +232,11 @@ public abstract Task RespondAsync(string text = null, Embed[] embeds = null, boo /// contains the sent message. /// public async Task RespondWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(fileStream, fileName)) { - await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } @@ -259,11 +259,11 @@ public async Task RespondWithFileAsync(Stream fileStream, string fileName, strin /// contains the sent message. /// public async Task RespondWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(filePath, fileName)) { - await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + await RespondWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } @@ -285,8 +285,8 @@ public async Task RespondWithFileAsync(string filePath, string fileName = null, /// contains the sent message. /// public Task RespondWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => RespondWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => RespondWithFilesAsync([attachment], text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); /// /// Responds to this interaction with a collection of file attachments. @@ -306,7 +306,7 @@ public Task RespondWithFileAsync(FileAttachment attachment, string text = null, /// contains the sent message. /// public abstract Task RespondWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -324,7 +324,7 @@ public abstract Task RespondWithFilesAsync(IEnumerable attachmen /// The sent message. /// public abstract Task FollowupAsync(string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Sends a followup message for this interaction. @@ -344,11 +344,11 @@ public abstract Task FollowupAsync(string text = null, Embe /// The sent message. /// public async Task FollowupWithFileAsync(Stream fileStream, string fileName, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(fileStream, fileName)) { - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } @@ -370,11 +370,11 @@ public async Task FollowupWithFileAsync(Stream fileStream, /// The sent message. /// public async Task FollowupWithFileAsync(string filePath, string fileName = null, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(filePath, fileName)) { - return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + return await FollowupWithFileAsync(file, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); } } @@ -396,8 +396,8 @@ public async Task FollowupWithFileAsync(string filePath, st /// contains the sent message. /// public Task FollowupWithFileAsync(FileAttachment attachment, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null) - => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None) + => FollowupWithFilesAsync(new FileAttachment[] { attachment }, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags); /// /// Sends a followup message for this interaction. @@ -417,7 +417,7 @@ public Task FollowupWithFileAsync(FileAttachment attachment /// contains the sent message. /// public abstract Task FollowupWithFilesAsync(IEnumerable attachments, string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, - AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null); + AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null, PollProperties poll = null, MessageFlags flags = MessageFlags.None); /// /// Gets the original response for this interaction. @@ -504,26 +504,26 @@ async Task IDiscordInteraction.ModifyOriginalResponseAsync(Action< => await ModifyOriginalResponseAsync(func, options).ConfigureAwait(false); /// async Task IDiscordInteraction.RespondAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, - Embed embed, RequestOptions options, PollProperties poll) - => await RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupAsync(string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, - MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// async Task IDiscordInteraction.FollowupWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, - bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); #if NETCOREAPP3_0_OR_GREATER != true /// - async Task IDiscordInteraction.FollowupWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + async Task IDiscordInteraction.FollowupWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// - async Task IDiscordInteraction.FollowupWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + async Task IDiscordInteraction.FollowupWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(filePath, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); /// - async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll) - => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll).ConfigureAwait(false); + async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options, PollProperties poll, MessageFlags flags) + => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options, poll, flags).ConfigureAwait(false); #endif #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 288a910889..4cba1d73d8 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -1,10 +1,13 @@ using Discord.Rest; + using Newtonsoft.Json.Linq; + using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; + using Model = Discord.API.Message; namespace Discord.WebSocket @@ -64,8 +67,8 @@ public abstract class SocketMessage : SocketEntity, IMessage /// public MessageReference Reference { get; private set; } - /// - public IReadOnlyCollection Components { get; private set; } + /// + public IReadOnlyCollection Components { get; private set; } /// /// Gets the interaction this message is a response to. @@ -211,62 +214,9 @@ internal virtual void Update(ClientState state, Model model) }; } - if (model.Components.IsSpecified) - { - Components = model.Components.Value.Where(x => x.Type is ComponentType.ActionRow) - .Select(x => new ActionRowComponent(((API.ActionRowComponent)x).Components.Select(y => - { - switch (y.Type) - { - case ComponentType.Button: - { - var parsed = (API.ButtonComponent)y; - return new Discord.ButtonComponent( - parsed.Style, - parsed.Label.GetValueOrDefault(), - parsed.Emote.IsSpecified - ? parsed.Emote.Value.Id.HasValue - ? new Emote(parsed.Emote.Value.Id.Value, parsed.Emote.Value.Name, parsed.Emote.Value.Animated.GetValueOrDefault()) - : new Emoji(parsed.Emote.Value.Name) - : null, - parsed.CustomId.GetValueOrDefault(), - parsed.Url.GetValueOrDefault(), - parsed.Disabled.GetValueOrDefault(), - parsed.SkuId.ToNullable()); - } - case ComponentType.SelectMenu: - { - var parsed = (API.SelectMenuComponent)y; - return new SelectMenuComponent( - parsed.CustomId, - parsed.Options.Select(z => new SelectMenuOption( - z.Label, - z.Value, - z.Description.GetValueOrDefault(), - z.Emoji.IsSpecified - ? z.Emoji.Value.Id.HasValue - ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) - : new Emoji(z.Emoji.Value.Name) - : null, - z.Default.ToNullable())).ToList(), - parsed.Placeholder.GetValueOrDefault(), - parsed.MinValues, - parsed.MaxValues, - parsed.Disabled, - parsed.Type, - parsed.ChannelTypes.GetValueOrDefault(), - parsed.DefaultValues.IsSpecified - ? parsed.DefaultValues.Value.Select(x => new SelectMenuDefaultValue(x.Id, x.Type)) - : Array.Empty() - ); - } - default: - return null; - } - }).ToList())).ToImmutableArray(); - } - else - Components = new List(); + Components = model.Components.IsSpecified + ? model.Components.Value.Select(x => x.ToEntity()).ToImmutableArray() + : []; if (model.UserMentions.IsSpecified) { @@ -324,7 +274,7 @@ internal virtual void Update(ClientState state, Model model) ? new GuildProductPurchase(model.PurchaseNotification.Value.ProductPurchase.Value.ListingId, model.PurchaseNotification.Value.ProductPurchase.Value.ProductName) : null); } - + if (model.Call.IsSpecified) CallData = new MessageCallData(model.Call.Value.Participants, model.Call.Value.EndedTimestamp.ToNullable()); } diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 47db9a21bb..b8861de93c 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -29,6 +29,11 @@ public static async Task SendMessageAsync(DiscordWebhookClient client, MessageFlags flags, ulong? threadId = null, string threadName = null, ulong[] appliedTags = null, PollProperties poll = null) { + if (components?.Components.Any(x => x.Type is not ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + + Preconditions.ValidateMessageFlags(flags); + var args = new CreateWebhookMessageParams { Content = text, @@ -48,7 +53,7 @@ public static async Task SendMessageAsync(DiscordWebhookClient client, if (allowedMentions != null) args.AllowedMentions = allowedMentions.ToModel(); if (components != null) - args.Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray(); + args.Components = components?.Components.Select(x => x.ToModel()).ToArray(); if (threadName is not null) args.ThreadName = threadName; if (appliedTags != null) @@ -56,8 +61,6 @@ public static async Task SendMessageAsync(DiscordWebhookClient client, if (poll != null) args.Poll = poll.ToModel(); - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options, threadId: threadId).ConfigureAwait(false); return model.Id; @@ -108,14 +111,14 @@ public static Task ModifyMessageAsync(DiscordWebhookClient client, ulong message AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), - Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, }; return client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId); } else { - var attachments = args.Attachments.Value?.ToArray() ?? Array.Empty(); + var attachments = args.Attachments.Value?.ToArray() ?? []; var apiArgs = new UploadWebhookFileParams(attachments) { @@ -127,7 +130,7 @@ public static Task ModifyMessageAsync(DiscordWebhookClient client, ulong message AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), - MessageComponents = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + MessageComponents = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, }; return client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId); @@ -193,8 +196,10 @@ public static async Task SendFilesAsync(DiscordWebhookClient client, } } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); + if (components?.Components.Any(x => x.Type is not ComponentType.ActionRow) ?? false) + flags |= MessageFlags.ComponentsV2; + + Preconditions.ValidateMessageFlags(flags); var args = new UploadWebhookFileParams(attachments.ToArray()) { @@ -204,7 +209,7 @@ public static async Task SendFilesAsync(DiscordWebhookClient client, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + MessageComponents = components?.Components.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, Flags = flags, ThreadName = threadName, AppliedTags = appliedTags,