From 202554fdde5ebcc38e36b0a1a55eb199cd6bfad9 Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Wed, 2 Mar 2022 18:31:52 -0400 Subject: [PATCH 001/153] [Robot] Add missing json error (#2152) * Add 30046, 40004, 40060, 50068, 50086 Error codes * Update src/Discord.Net.Core/DiscordErrorCode.cs Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 03f8b19e9..3eb9637eb 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -96,9 +96,11 @@ namespace Discord #endregion #region General Request Errors (40XXX) + MaximumNumberOfEditsReached = 30046, TokenUnauthorized = 40001, InvalidVerification = 40002, OpeningDMTooFast = 40003, + SendMessagesHasBeenTemporarilyDisabled = 40004, RequestEntityTooLarge = 40005, FeatureDisabled = 40006, UserBanned = 40007, @@ -108,6 +110,7 @@ namespace Discord #endregion #region Action Preconditions/Checks (50XXX) + InteractionHasAlreadyBeenAcknowledged = 40060, MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, @@ -141,12 +144,14 @@ namespace Discord InvalidFileUpload = 50046, CannotSelfRedeemGift = 50054, InvalidGuild = 50055, + InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, CannotDeleteRequiredCommunityChannel = 50074, InvalidSticker = 50081, CannotExecuteOnArchivedThread = 50083, InvalidThreadNotificationSettings = 50084, BeforeValueEarlierThanThreadCreation = 50085, + CommunityServerChannelsMustBeTextChannels = 50086, ServerLocaleUnavailable = 50095, ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, From 1dc473c7e41226d986ab7b12f35f88d501b8068e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:08 -0400 Subject: [PATCH 002/153] Add Image property to Guild Scheduled Events (#2151) * Add Image property to create and modify events * Add CDN routes to get cover image * Update banner names * Update CDN.cs * Update IGuildScheduledEvent.cs --- src/Discord.Net.Core/CDN.cs | 12 ++++++++++++ .../Guilds/GuildScheduledEventsProperties.cs | 5 +++++ src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 2 ++ .../Entities/Guilds/IGuildScheduledEvent.cs | 13 +++++++++++++ .../API/Common/GuildScheduledEvent.cs | 2 ++ .../API/Rest/CreateGuildScheduledEventParams.cs | 2 ++ .../API/Rest/ModifyGuildScheduledEventParams.cs | 2 ++ src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 12 ++++++++++-- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 8 +++++--- .../Entities/Guilds/RestGuildEvent.cs | 8 ++++++++ .../Entities/Guilds/SocketGuild.cs | 8 +++++--- .../Entities/Guilds/SocketGuildEvent.cs | 8 ++++++++ 12 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d6535a4f1..1a8795101 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -208,6 +208,18 @@ namespace Discord public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png) => $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}"; + /// + /// Returns an events cover image url. + /// + /// The guild id that the event is in. + /// The id of the event. + /// The id of the cover image asset. + /// The format of the image. + /// The size of the image. + /// + public static string GetEventCoverImageUrl(ulong guildId, ulong eventId, string assetId, ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => $"{DiscordConfig.CDNUrl}guild-events/{guildId}/{eventId}/{assetId}.{FormatToExtension(format, assetId)}?size={size}"; + private static string FormatToExtension(StickerFormatType format) { return format switch diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs index a3fd729e5..d3be8b784 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs @@ -54,5 +54,10 @@ namespace Discord /// Gets or sets the status of the event. /// public Optional Status { get; set; } + + /// + /// Gets or sets the banner image of the event. + /// + public Optional CoverImage { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index ae1b2d67d..3111ff495 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1105,6 +1105,7 @@ namespace Discord /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1118,6 +1119,7 @@ namespace Discord DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index e50f4cc2b..4b2fa3bee 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -39,6 +39,11 @@ namespace Discord /// string Description { get; } + /// + /// Gets the banner asset id of the event. + /// + string CoverImageId { get; } + /// /// Gets the start time of the event. /// @@ -80,6 +85,14 @@ namespace Discord /// int? UserCount { get; } + /// + /// Gets this events banner image url. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The cover images url. + string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); + /// /// Starts the event. /// diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs index 338c24dc9..94c53e779 100644 --- a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs @@ -39,5 +39,7 @@ namespace Discord.API public Optional Creator { get; set; } [JsonProperty("user_count")] public Optional UserCount { get; set; } + [JsonProperty("image")] + public string Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs index a207d3374..2ccd06fe6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs @@ -25,5 +25,7 @@ namespace Discord.API.Rest public Optional Description { get; set; } [JsonProperty("entity_type")] public GuildScheduledEventType Type { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs index 3d191a0b3..1179ddcbe 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs @@ -27,5 +27,7 @@ namespace Discord.API.Rest public Optional Type { get; set; } [JsonProperty("status")] public Optional Status { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 874d3c2cd..25f474dcc 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -799,7 +799,12 @@ namespace Discord.Rest PrivacyLevel = args.PrivacyLevel, StartTime = args.StartTime, Status = args.Status, - Type = args.Type + Type = args.Type, + Image = args.CoverImage.IsSpecified + ? args.CoverImage.Value.HasValue + ? args.CoverImage.Value.Value.ToModel() + : null + : Optional.Unspecified }; if(args.Location.IsSpecified) @@ -839,6 +844,7 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? bannerImage = null, RequestOptions options = null) { if(location != null) @@ -864,6 +870,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, @@ -872,7 +879,8 @@ namespace Discord.Rest Name = name, PrivacyLevel = privacyLevel, StartTime = startTime, - Type = type + Type = type, + Image = bannerImage.HasValue ? bannerImage.Value.ToModel() : Optional.Unspecified }; if(location != null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index d90372636..2c37bb2da 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1167,6 +1167,7 @@ namespace Discord.Rest /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1180,8 +1181,9 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) - => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); #endregion @@ -1198,8 +1200,8 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Stickers => Stickers; /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs index d3ec11fc6..0b02e60ba 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs @@ -28,6 +28,9 @@ namespace Discord.Rest /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -98,8 +101,13 @@ namespace Discord.Rest EntityId = model.EntityId; Location = model.EntityMetadata?.Location.GetValueOrDefault(); UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task StartAsync(RequestOptions options = null) => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 4a7d4fafb..b38dfcd74 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1295,6 +1295,7 @@ namespace Discord.WebSocket /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1308,6 +1309,7 @@ namespace Discord.WebSocket DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) { // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements @@ -1324,7 +1326,7 @@ namespace Discord.WebSocket break; } - return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); } @@ -1803,8 +1805,8 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuild.Stickers => Stickers; /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index df619e4ca..a86aafadf 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -35,6 +35,9 @@ namespace Discord.WebSocket /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -109,8 +112,13 @@ namespace Discord.WebSocket StartTime = model.ScheduledStartTime; Status = model.Status; UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteEventAsync(Discord, this, options); From 6bf5818e72fbdad1c2c2cf8cd588d6e7c5f21cb7 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:29 -0400 Subject: [PATCH 003/153] Add IsInvitable and CreatedAt to threads (#2153) * Add IsInvitable and CreatedAt to threads * Update src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs Co-Authored-By: Jared L <48422312+lhjt@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../Entities/Channels/IThreadChannel.cs | 17 +++++++++++++++++ .../API/Common/ThreadMetadata.cs | 6 ++++++ .../Entities/Channels/RestChannel.cs | 2 +- .../Entities/Channels/RestThreadChannel.cs | 16 +++++++++++++--- .../Entities/Channels/SocketChannel.cs | 2 +- .../Entities/Channels/SocketThreadChannel.cs | 17 +++++++++++++---- 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index 50e46efa6..f03edbbf9 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -48,6 +48,23 @@ namespace Discord /// int MessageCount { get; } + /// + /// Gets whether non-moderators can add other non-moderators to a thread. + /// + /// + /// This property is only available on private threads. + /// + bool? IsInvitable { get; } + + /// + /// Gets when the thread was created. + /// + /// + /// This property is only populated for threads created after 2022-01-09, hence the default date of this + /// property will be that date. + /// + new DateTimeOffset CreatedAt { get; } + /// /// Joins the current thread. /// diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs index 39e9bd13e..15854fab4 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -16,5 +16,11 @@ namespace Discord.API [JsonProperty("locked")] public Optional Locked { get; set; } + + [JsonProperty("invitable")] + public Optional Invitable { get; set; } + + [JsonProperty("create_timestamp")] + public Optional CreatedAt { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 83c6d8bfb..c730596c7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -13,7 +13,7 @@ namespace Discord.Rest { #region RestChannel /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index 63071b9a5..c763a6660 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -34,17 +34,26 @@ namespace Discord.Rest /// public int MessageCount { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets the parent text channel id. /// public ulong ParentChannelId { get; private set; } - internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id) - : base(discord, guild, id) { } + internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id, DateTimeOffset? createdAt) + : base(discord, guild, id) + { + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); + } internal new static RestThreadChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestThreadChannel(discord, guild, model.Id); + var entity = new RestThreadChannel(discord, guild, model.Id, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault()); entity.Update(model); return entity; } @@ -57,6 +66,7 @@ namespace Discord.Rest if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 758ee9271..c30b3d254 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -17,7 +17,7 @@ namespace Discord.WebSocket /// /// Gets when the channel is created. /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// /// Gets a collection of users from the WebSocket cache. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 7fcafc14a..c26a23afd 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -44,7 +44,7 @@ namespace Discord.WebSocket /// /// Gets the parent channel this thread resides in. /// - public SocketTextChannel ParentChannel { get; private set; } + public SocketGuildChannel ParentChannel { get; private set; } /// public int MessageCount { get; private set; } @@ -64,6 +64,12 @@ namespace Discord.WebSocket /// public bool IsLocked { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets a collection of cached users within this thread. /// @@ -78,17 +84,19 @@ namespace Discord.WebSocket private readonly object _downloadLock = new object(); - internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) + internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketGuildChannel parent, + DateTimeOffset? createdAt) : base(discord, id, guild) { ParentChannel = parent; _members = new ConcurrentDictionary(); + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); } internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) { - var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value); - var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent); + var parent = guild.GetChannel(model.CategoryId.Value); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.ToNullable()); entity.Update(state, model); return entity; } @@ -103,6 +111,7 @@ namespace Discord.WebSocket if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; From b3370c33e2eb1ccfe5011989aff472b0f9a84eb7 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:59 -0400 Subject: [PATCH 004/153] Fix usage of CacheMode.AllowDownload in channels (#2154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: ✨ <25006819+sabihoshi@users.noreply.github.com> Co-authored-by: ✨ <25006819+sabihoshi@users.noreply.github.com> --- .../Entities/Channels/IChannel.cs | 2 +- .../Entities/Channels/RestGuildChannel.cs | 2 +- .../Entities/Channels/RestTextChannel.cs | 9 ++--- src/Discord.Net.WebSocket/BaseSocketClient.cs | 17 ++++++--- .../DiscordShardedClient.cs | 11 +++++- .../DiscordSocketClient.cs | 15 ++++++-- .../Channels/SocketCategoryChannel.cs | 38 +++++++++++++++---- .../Entities/Channels/SocketGroupChannel.cs | 2 +- .../Entities/Channels/SocketGuildChannel.cs | 4 +- .../Entities/Channels/SocketTextChannel.cs | 19 ++++++++-- .../Entities/Guilds/SocketGuild.cs | 13 +++++-- 11 files changed, 97 insertions(+), 35 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index e2df86f2a..6d58486f8 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -21,7 +21,7 @@ namespace Discord /// /// /// - /// The returned collection is an asynchronous enumerable object; one must call + /// The returned collection is an asynchronous enumerable object; one must call /// to access the individual messages as a /// collection. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index bc9d4110a..fa2362854 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -227,7 +227,7 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice + => AsyncEnumerable.Empty>(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index f1bdee65c..198ff22ac 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -261,7 +261,7 @@ namespace Discord.Rest /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -364,10 +364,9 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); + return mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); } #endregion diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 20acd85dd..bb2d489b4 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -209,7 +209,7 @@ namespace Discord.WebSocket /// Sets the of the logged-in user. /// /// - /// This method sets the of the user. + /// This method sets the of the user. /// /// Discord will only accept setting of name and the type of activity. /// @@ -219,7 +219,7 @@ namespace Discord.WebSocket /// /// /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC - /// clients only. + /// clients only. /// /// /// The activity to be set. @@ -240,7 +240,7 @@ namespace Discord.WebSocket /// Creates a guild for the logged-in user who is in less than 10 active guilds. /// /// - /// This method creates a new guild on behalf of the logged-in user. + /// This method creates a new guild on behalf of the logged-in user. /// /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. /// @@ -317,8 +317,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 51c6d3c34..8374f2877 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -533,8 +533,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b0215d9ef..cd40a491f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -543,7 +543,7 @@ namespace Discord.WebSocket if(model == null) return null; - + if (model.GuildId.IsSpecified) { var guild = State.GetGuild(model.GuildId.Value); @@ -2128,7 +2128,7 @@ namespace Discord.WebSocket { await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); } - } + } } await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); @@ -2520,7 +2520,7 @@ namespace Discord.WebSocket } break; - case "THREAD_MEMBERS_UPDATE": + case "THREAD_MEMBERS_UPDATE": { await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); @@ -3113,7 +3113,14 @@ namespace Discord.WebSocket /// async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetUserAsync(id, options).ConfigureAwait(false) : GetUser(id); + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 9c7dd4fbd..43f23de1a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Discord.Rest; using Model = Discord.API.Channel; namespace Discord.WebSocket @@ -64,21 +65,44 @@ namespace Discord.WebSocket #endregion #region IGuildChannel + /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, + RequestOptions options) + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index c8137784f..afb133ac2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -352,7 +352,7 @@ namespace Discord.WebSocket Task IAudioChannel.ModifyAsync(Action func, RequestOptions options) { throw new NotSupportedException(); } #endregion - #region IChannel + #region IChannel /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 45eb28444..79f02fe1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -214,10 +214,10 @@ namespace Discord.WebSocket /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + => Task.FromResult(GetUser(id)); //Overridden in Text/Voice #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index dbf238625..9591f68fe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -103,7 +103,7 @@ namespace Discord.WebSocket /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -355,11 +355,22 @@ namespace Discord.WebSocket #region IGuildChannel /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } + #endregion #region IMessageChannel diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index b38dfcd74..bd5d811f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -372,7 +372,7 @@ namespace Discord.WebSocket /// This field is based off of caching alone, since there is no events returned on the guild model. /// /// - /// A read-only collection of guild events found within this guild. + /// A read-only collection of guild events found within this guild. /// public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); @@ -1928,8 +1928,15 @@ namespace Discord.WebSocket async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) => await AddGuildUserAsync(userId, accessToken, func, options); /// - Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await GuildHelper.GetUserAsync(this, Discord, id, options).ConfigureAwait(false); + } + /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); From 1fb62de14b071f0d2a4b261ae7344d3a586c4e41 Mon Sep 17 00:00:00 2001 From: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Date: Wed, 2 Mar 2022 17:23:27 -0600 Subject: [PATCH 005/153] Support Sending Message Flags (#2131) * Add message flags * Add webhook message flags --- .../Entities/Channels/IMessageChannel.cs | 15 ++- .../API/Rest/CreateMessageParams.cs | 3 + .../Entities/Channels/ChannelHelper.cs | 58 +++++++++--- .../Entities/Channels/IRestMessageChannel.cs | 86 ++--------------- .../Entities/Channels/RestDMChannel.cs | 79 ++++++++++++---- .../Entities/Channels/RestGroupChannel.cs | 90 +++++++++++++----- .../Entities/Channels/RestTextChannel.cs | 76 +++++++++++---- .../Channels/ISocketMessageChannel.cs | 93 ++++--------------- .../Entities/Channels/SocketDMChannel.cs | 81 ++++++++++++---- .../Entities/Channels/SocketGroupChannel.cs | 80 ++++++++++++---- .../Entities/Channels/SocketTextChannel.cs | 89 +++++++++++++----- .../DiscordWebhookClient.cs | 23 +++-- .../WebhookClientHelper.cs | 42 +++++++-- .../MockedEntities/MockedDMChannel.cs | 10 +- .../MockedEntities/MockedGroupChannel.cs | 10 +- .../MockedEntities/MockedTextChannel.cs | 13 +-- 16 files changed, 515 insertions(+), 333 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 00ec38746..60a7c7575 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -31,11 +31,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the message. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -71,11 +72,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -108,11 +110,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a file to this message channel with an optional caption. /// @@ -137,11 +140,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Sends a collection of files to this message channel. /// @@ -166,11 +170,12 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index 5996c7e83..466ad41e3 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -28,6 +28,9 @@ namespace Discord.API.Rest [JsonProperty("sticker_ids")] public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index b5087cd2f..d66fd5e51 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -266,8 +266,10 @@ namespace Discord.Rest } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -298,6 +300,10 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } + + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var args = new CreateMessageParams(text) { IsTTS = isTTS, @@ -305,7 +311,8 @@ namespace Discord.Rest AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel(), Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); @@ -335,29 +342,44 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) + string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) + Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler)) - return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) - => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags = MessageFlags.None) + => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + /// The only valid are and . public static async Task SendFilesAsync(IMessageChannel channel, BaseDiscordClient client, - IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) + IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -366,7 +388,7 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); - + foreach(var attachment in attachments) { Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); @@ -398,12 +420,26 @@ namespace Discord.Rest } } + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (stickers != null) { Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - var args = new UploadFileParams(attachments.ToArray()) { Content = text, IsTTS = isTTS, 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, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified }; + var args = new UploadFileParams(attachments.ToArray()) + { + Content = text, + IsTTS = isTTS, + 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, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags + }; + var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 1af936a57..0cf92bb04 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -9,84 +9,14 @@ namespace Discord.Rest /// public interface IRestMessageChannel : IMessageChannel { - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in - /// . Please visit - /// its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 36b190e56..3bf43a594 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -94,8 +94,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -122,22 +126,39 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -219,20 +240,38 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 03858fbbe..d21852f93 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -104,8 +104,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -132,20 +136,40 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -197,17 +221,41 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IAudioChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 198ff22ac..76c75ab6e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -103,8 +103,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -131,23 +135,42 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -332,24 +355,37 @@ namespace Discord.Rest => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IGuildChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 3e9b635de..b632bcb60 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -18,83 +18,22 @@ namespace Discord.WebSocket /// IReadOnlyCollection CachedMessages { get; } - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the message. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// The message components to be included with this message. Used for interactions. - /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. - /// - /// A task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null); + /// + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a cached message from this channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index f4fe12755..17ab4ebe3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -139,24 +139,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -255,20 +279,37 @@ namespace Discord.WebSocket async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index afb133ac2..4f068cf81 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -178,24 +178,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -327,21 +351,37 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IAudioChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 9591f68fe..e4a299edc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -212,27 +212,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - - /// - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + + /// + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) @@ -396,20 +417,38 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region INestedChannel diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index f7bc38587..405100f89 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -87,8 +87,9 @@ namespace Discord.Webhook /// Sends a message to the channel for this webhook. /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, - string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components); + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, MessageFlags flags = MessageFlags.None) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); /// /// Modifies a message posted using this webhook. @@ -124,33 +125,35 @@ namespace Discord.Webhook public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components); + allowedMentions, options, isSpoiler, components, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components); + avatarUrl, allowedMentions, options, isSpoiler, components, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options); + avatarUrl, allowedMentions, components, options, flags); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options); + allowedMentions, components, options, flags); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index a9d5a25da..0a974a9d9 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,12 +21,14 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, MessageComponent components) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) { var args = new CreateWebhookMessageParams { Content = text, - IsTTS = isTTS + IsTTS = isTTS, + Flags = flags }; if (embeds != null) @@ -40,6 +42,9 @@ namespace Discord.Webhook if (components != null) args.Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray(); + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); return model.Id; } @@ -97,22 +102,27 @@ namespace Discord.Webhook await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); } public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, - MessageComponent components) - => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + MessageComponent components, MessageFlags flags) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); - public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) - => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, + MessageComponent components, RequestOptions options, MessageFlags flags) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); public static async Task SendFilesAsync(DiscordWebhookClient client, - IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) + IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, + string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, + MessageFlags flags) { embeds ??= Array.Empty(); @@ -141,7 +151,19 @@ namespace Discord.Webhook } } - var args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, 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 }; + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + var args = new UploadWebhookFileParams(attachments.ToArray()) + { + AvatarUrl = avatarUrl, + Username = username, 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, + Flags = flags + }; var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 519bab4d9..2a7f8065a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,10 +83,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 9c94efffa..b7f98f572 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -93,17 +93,17 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -113,7 +113,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ad0af04b2..0dfcab7a5 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -176,17 +176,17 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -211,9 +211,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); } } From 9ba64f62d1013387231a98c00dc5d1dbb86bdb23 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:23:39 -0400 Subject: [PATCH 006/153] Interaction Service Complex Parameters (#2155) * Interaction Service Complex Parameters * add complex parameters * add complex parameters * fix build errors * add argument parsing * add nested complex parameter checks * add inline docs * add preferred constructor declaration * fix autocompletehandlers for complex parameters * make GetConstructor private * use flattened params in ToProps method * make DiscordType of SlashParameter nullable * add docs to Flattened parameters collection and move the GetComplexParameterCtor method * add inline docs to SlashCommandParameterBuilder.ComplexParameterFields * add check for validating required/optinal parameter order * implement change requests * return internal ParseResult as ExecuteResult Co-Authored-By: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> * fix merge errors Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> --- .../Attributes/ComplexParameterAttribute.cs | 30 +++++ .../ComplexParameterCtorAttribute.cs | 10 ++ .../Builders/ModuleClassBuilder.cs | 57 +++++++++- .../SlashCommandParameterBuilder.cs | 68 +++++++++++- .../Info/Commands/CommandInfo.cs | 3 + .../Info/Commands/SlashCommandInfo.cs | 104 ++++++++++++++---- .../Parameters/SlashCommandParameterInfo.cs | 30 ++++- .../InteractionService.cs | 4 +- .../Results/ParseResult.cs | 36 ++++++ .../Utilities/ApplicationCommandRestUtil.cs | 6 +- 10 files changed, 315 insertions(+), 33 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs create mode 100644 src/Discord.Net.Interactions/Results/ParseResult.cs diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs new file mode 100644 index 000000000..952ca06a4 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Registers a parameter as a complex parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class ComplexParameterAttribute : Attribute + { + /// + /// Gets the parameter array of the constructor method that should be prioritized. + /// + public Type[] PrioritizedCtorSignature { get; } + + /// + /// Registers a slash command parameter as a complex parameter. + /// + public ComplexParameterAttribute() { } + + /// + /// Registers a slash command parameter as a complex parameter with a specified constructor signature. + /// + /// Type array of the preferred constructor parameters. + public ComplexParameterAttribute(Type[] types) + { + PrioritizedCtorSignature = types; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs new file mode 100644 index 000000000..59ee3377b --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Tag a type constructor as the preferred Complex command constructor. + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)] + public class ComplexParameterCtorAttribute : Attribute { } +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 6615f131c..88a34f3b2 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -397,7 +397,6 @@ namespace Discord.Interactions.Builders builder.Description = paramInfo.Name; builder.IsRequired = !paramInfo.IsOptional; builder.DefaultValue = paramInfo.DefaultValue; - builder.SetParameterType(paramType, services); foreach (var attribute in attributes) { @@ -435,12 +434,32 @@ namespace Discord.Interactions.Builders case MinValueAttribute minValue: builder.MinValue = minValue.Value; break; + case ComplexParameterAttribute complexParameter: + { + builder.IsComplexParameter = true; + ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); + + foreach (var parameter in ctor.GetParameters()) + { + if (parameter.IsDefined(typeof(ComplexParameterAttribute))) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); + } + + var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? + ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; + builder.ComplexParameterInitializer = args => initializer(args); + } + break; default: builder.AddAttributes(attribute); break; } } + builder.SetParameterType(paramType, services); + // Replace pascal casings with '-' builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); } @@ -608,5 +627,41 @@ namespace Discord.Interactions.Builders propertyInfo.SetMethod?.IsStatic == false && propertyInfo.IsDefined(typeof(ModalInputAttribute)); } + + private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) + { + var ctors = typeInfo.GetConstructors(); + + if (ctors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); + + if (complexParameter.PrioritizedCtorSignature is not null) + { + var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); + + if (ctor is null) + throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); + + return ctor; + } + + var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); + + switch (prioritizedCtors.Count()) + { + case > 1: + throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); + case 1: + return prioritizedCtors.First(); + } + + switch (ctors.Length) + { + case > 1: + throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); + default: + return ctors.First(); + } + } } } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs index c208a4b0e..d600c9cc7 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Interactions.Builders { @@ -10,6 +11,7 @@ namespace Discord.Interactions.Builders { private readonly List _choices = new(); private readonly List _channelTypes = new(); + private readonly List _complexParameterFields = new(); /// /// Gets or sets the description of this parameter. @@ -36,6 +38,11 @@ namespace Discord.Interactions.Builders /// public IReadOnlyCollection ChannelTypes => _channelTypes; + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields => _complexParameterFields; + /// /// Gets or sets whether this parameter should be configured for Autocomplete Interactions. /// @@ -46,6 +53,16 @@ namespace Discord.Interactions.Builders /// public TypeConverter TypeConverter { get; private set; } + /// + /// Gets whether this type should be treated as a complex parameter. + /// + public bool IsComplexParameter { get; internal set; } + + /// + /// Gets the initializer delegate for this parameter, if is . + /// + public ComplexParameterInitializer ComplexParameterInitializer { get; internal set; } + /// /// Gets or sets the of this parameter. /// @@ -60,7 +77,14 @@ namespace Discord.Interactions.Builders /// Parent command of this parameter. /// Name of this command. /// Type of this parameter. - public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type, ComplexParameterInitializer complexParameterInitializer = null) + : base(command, name, type) + { + ComplexParameterInitializer = complexParameterInitializer; + + if (complexParameterInitializer is not null) + IsComplexParameter = true; + } /// /// Sets . @@ -168,7 +192,47 @@ namespace Discord.Interactions.Builders public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) { base.SetParameterType(type); - TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + if(!IsComplexParameter) + TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + return this; + } + + /// + /// Adds a parameter builders to . + /// + /// factory. + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterField(Action configure) + { + SlashCommandParameterBuilder builder = new(Command); + configure(builder); + + if(builder.IsComplexParameter) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.Add(builder); + return this; + } + + /// + /// Adds parameter builders to . + /// + /// New parameter builders to be added to . + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterFields(params SlashCommandParameterBuilder[] fields) + { + if(fields.Any(x => x.IsComplexParameter)) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.AddRange(fields); return this; } diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index cf1a2dfa1..49ad009c9 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -31,6 +31,8 @@ namespace Discord.Interactions private readonly ExecuteCallback _action; private readonly ILookup _groupedPreconditions; + internal IReadOnlyDictionary _parameterDictionary { get; } + /// public ModuleInfo Module { get; } @@ -79,6 +81,7 @@ namespace Discord.Interactions _action = builder.Callback; _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); + _parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index 116a07ab4..456ad4bfe 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -13,6 +13,8 @@ namespace Discord.Interactions /// public class SlashCommandInfo : CommandInfo, IApplicationCommandInfo { + internal IReadOnlyDictionary _flattenedParameterDictionary { get; } + /// /// Gets the command description that will be displayed on Discord. /// @@ -30,11 +32,23 @@ namespace Discord.Interactions /// public override bool SupportsWildCards => false; + /// + /// Gets the flattened collection of command parameters and complex parameter fields. + /// + public IReadOnlyCollection FlattenedParameters { get; } + internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) { Description = builder.Description; DefaultPermission = builder.DefaultPermission; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); + + for (var i = 0; i < FlattenedParameters.Count - 1; i++) + if (!FlattenedParameters.ElementAt(i).IsRequired && FlattenedParameters.ElementAt(i + 1).IsRequired) + throw new InvalidOperationException("Optional parameters must appear after all required parameters, ComplexParameters with optional parameters must be located at the end."); + + _flattenedParameterDictionary = FlattenedParameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// @@ -56,45 +70,81 @@ namespace Discord.Interactions { try { - if (paramList?.Count() < argList?.Count()) - return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters"); - var args = new object[paramList.Count()]; for (var i = 0; i < paramList.Count(); i++) { var parameter = paramList.ElementAt(i); - var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); - if (arg == default) + if(!result.IsSuccess) { - if (parameter.IsRequired) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); - else - args[i] = parameter.DefaultValue; + var execResult = ExecuteResult.FromError(result); + await InvokeModuleEvent(context, execResult).ConfigureAwait(false); + return execResult; } + + if (result is ParseResult parseResult) + args[i] = parseResult.Value; else - { - var typeConverter = parameter.TypeConverter; + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); + } - var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + return await RunAsync(context, args, services).ConfigureAwait(false); + } + catch (Exception ex) + { + var result = ExecuteResult.FromError(ex); + await InvokeModuleEvent(context, result).ConfigureAwait(false); + return result; + } + } - if (!readResult.IsSuccess) - { - await InvokeModuleEvent(context, readResult).ConfigureAwait(false); - return readResult; - } + private async Task ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List argList, + IServiceProvider services) + { + if (parameterInfo.IsComplexParameter) + { + var ctorArgs = new object[parameterInfo.ComplexParameterFields.Count]; - args[i] = readResult.Value; - } + for (var i = 0; i < ctorArgs.Length; i++) + { + var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + if (result is ParseResult parseResult) + ctorArgs[i] = parseResult.Value; + else + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); } - return await RunAsync(context, args, services).ConfigureAwait(false); + return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); } - catch (Exception ex) + else { - return ExecuteResult.FromError(ex); + var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); + + if (arg == default) + { + if (parameterInfo.IsRequired) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); + else + return ParseResult.FromSuccess(parameterInfo.DefaultValue); + } + else + { + var typeConverter = parameterInfo.TypeConverter; + + var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + + if (!readResult.IsSuccess) + return readResult; + + return ParseResult.FromSuccess(readResult.Value); + } } } @@ -108,5 +158,15 @@ namespace Discord.Interactions else return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Channel}"; } + + private static IEnumerable FlattenParameters(IEnumerable parameters) + { + foreach (var parameter in parameters) + if (!parameter.IsComplexParameter) + yield return parameter; + else + foreach(var complexParameterField in parameter.ComplexParameterFields) + yield return complexParameterField; + } } } diff --git a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs index 68b63c806..8702d69f7 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs @@ -1,13 +1,25 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Discord.Interactions { + /// + /// Represents a cached argument constructor delegate. + /// + /// Method arguments array. + /// + /// Returns the constructed object. + /// + public delegate object ComplexParameterInitializer(object[] args); + /// /// Represents the parameter info class for commands. /// public class SlashCommandParameterInfo : CommandParameterInfo { + internal readonly ComplexParameterInitializer _complexParameterInitializer; + /// public new SlashCommandInfo Command => base.Command as SlashCommandInfo; @@ -43,9 +55,14 @@ namespace Discord.Interactions public bool IsAutocomplete { get; } /// - /// Gets the Discord option type this parameter represents. + /// Gets whether this type should be treated as a complex parameter. /// - public ApplicationCommandOptionType DiscordOptionType => TypeConverter.GetDiscordType(); + public bool IsComplexParameter { get; } + + /// + /// Gets the Discord option type this parameter represents. If the parameter is not a complex parameter. + /// + public ApplicationCommandOptionType? DiscordOptionType => TypeConverter?.GetDiscordType(); /// /// Gets the parameter choices of this Slash Application Command parameter. @@ -57,6 +74,11 @@ namespace Discord.Interactions /// public IReadOnlyCollection ChannelTypes { get; } + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields { get; } + internal SlashCommandParameterInfo(Builders.SlashCommandParameterBuilder builder, SlashCommandInfo command) : base(builder, command) { TypeConverter = builder.TypeConverter; @@ -64,9 +86,13 @@ namespace Discord.Interactions Description = builder.Description; MaxValue = builder.MaxValue; MinValue = builder.MinValue; + IsComplexParameter = builder.IsComplexParameter; IsAutocomplete = builder.Autocomplete; Choices = builder.Choices.ToImmutableArray(); ChannelTypes = builder.ChannelTypes.ToImmutableArray(); + ComplexParameterFields = builder.ComplexParameterFields?.Select(x => x.Build(command)).ToImmutableArray(); + + _complexParameterInitializer = builder.ComplexParameterInitializer; } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index c1291bd6b..bf56eddc5 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -747,9 +747,7 @@ namespace Discord.Interactions if(autocompleteHandlerResult.IsSuccess) { - var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal)); - - if(parameter?.AutocompleteHandler is not null) + if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Interactions/Results/ParseResult.cs b/src/Discord.Net.Interactions/Results/ParseResult.cs new file mode 100644 index 000000000..dfc6a57fe --- /dev/null +++ b/src/Discord.Net.Interactions/Results/ParseResult.cs @@ -0,0 +1,36 @@ +using System; + +namespace Discord.Interactions +{ + internal struct ParseResult : IResult + { + public object Value { get; } + + public InteractionCommandError? Error { get; } + + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private ParseResult(object value, InteractionCommandError? error, string reason) + { + Value = value; + Error = error; + ErrorReason = reason; + } + + public static ParseResult FromSuccess(object value) => + new ParseResult(value, null, null); + + public static ParseResult FromError(Exception exception) => + new ParseResult(null, InteractionCommandError.Exception, exception.Message); + + public static ParseResult FromError(InteractionCommandError error, string reason) => + new ParseResult(null, error, reason); + + public static ParseResult FromError(IResult result) => + new ParseResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 48b6e44e7..46f0f4a4a 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -13,7 +13,7 @@ namespace Discord.Interactions { Name = parameterInfo.Name, Description = parameterInfo.Description, - Type = parameterInfo.DiscordOptionType, + Type = parameterInfo.DiscordOptionType.Value, IsRequired = parameterInfo.IsRequired, Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { @@ -46,7 +46,7 @@ namespace Discord.Interactions if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); - props.Options = commandInfo.Parameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; + props.Options = commandInfo.FlattenedParameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; return props; } @@ -58,7 +58,7 @@ namespace Discord.Interactions Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.Parameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() }; public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) From c80067425a6e059c9862c40b47772e056e0b0eda Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:23:51 -0400 Subject: [PATCH 007/153] Display name support for enum type converter (#2156) * Display name support for enum type converter * allow display attribute on enum type converter * update docs/examples to include enum Display sample * Revert "allow display attribute on enum type converter" This reverts commit a0eec5b7555d366f9de7421f6fcf6bc71f2a4557. * adds ChoiceDisplay for enum type converters * Update EnumChoiceAttribute.cs * fix renamed folder issue * fix namespace Co-authored-by: Xeno --- .../samples/intro/groupattribute.cs | 8 +++--- samples/InteractionFramework/ExampleEnum.cs | 12 ++++++++- .../Attributes/EnumChoiceAttribute.cs | 25 +++++++++++++++++++ .../TypeConverters/EnumConverter.cs | 7 ++++-- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs diff --git a/docs/guides/int_framework/samples/intro/groupattribute.cs b/docs/guides/int_framework/samples/intro/groupattribute.cs index 86a492c31..99d6cd67b 100644 --- a/docs/guides/int_framework/samples/intro/groupattribute.cs +++ b/docs/guides/int_framework/samples/intro/groupattribute.cs @@ -1,16 +1,18 @@ [SlashCommand("blep", "Send a random adorable animal photo")] -public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Penguin", "penguin")] string animal) +public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal) { ... } -// In most cases, you can use an enum to replace the seperate choice attributes in a command. +// In most cases, you can use an enum to replace the separate choice attributes in a command. public enum Animal { Cat, Dog, - Penguin + // You can also use the ChoiceDisplay attribute to change how they appear in the choice menu. + [ChoiceDisplay("Guinea pig")] + GuineaPig } [SlashCommand("blep", "Send a random adorable animal photo")] diff --git a/samples/InteractionFramework/ExampleEnum.cs b/samples/InteractionFramework/ExampleEnum.cs index 755f33d17..a70dd49a9 100644 --- a/samples/InteractionFramework/ExampleEnum.cs +++ b/samples/InteractionFramework/ExampleEnum.cs @@ -1,3 +1,11 @@ +using Discord.Interactions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + namespace InteractionFramework { public enum ExampleEnum @@ -5,6 +13,8 @@ namespace InteractionFramework First, Second, Third, - Fourth + Fourth, + [ChoiceDisplay("Twenty First")] + TwentyFirst } } diff --git a/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs new file mode 100644 index 000000000..c7f83b6cd --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Customize the displayed value of a slash command choice enum. Only works with the default enum type converter. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public class ChoiceDisplayAttribute : Attribute + { + /// + /// Gets the name of the parameter. + /// + public string Name { get; } = null; + + /// + /// Modify the default name and description values of a Slash Command parameter. + /// + /// Name of the parameter. + public ChoiceDisplayAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs index a06c70ec4..1406c6f1a 100644 --- a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs @@ -2,6 +2,7 @@ using Discord.WebSocket; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; namespace Discord.Interactions @@ -27,12 +28,14 @@ namespace Discord.Interactions var choices = new List(); foreach (var member in members) + { + var displayValue = member.GetCustomAttribute()?.Name ?? member.Name; choices.Add(new ApplicationCommandOptionChoiceProperties { - Name = member.Name, + Name = displayValue, Value = member.Name }); - + } properties.Choices = choices; } } From 507a18d389abb5931e1b4d0dcca06694ba3a6258 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:24:00 -0400 Subject: [PATCH 008/153] Enforce valid button styles (#2157) Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> --- .../Interactions/MessageComponents/ComponentBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 0fa8189c1..7becca0e0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -613,6 +613,9 @@ namespace Discord if (!(string.IsNullOrEmpty(Url) ^ string.IsNullOrEmpty(CustomId))) throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!"); + if (Style == 0) + throw new ArgumentException("A button must have a style.", nameof(Style)); + if (Style == ButtonStyle.Link) { if (string.IsNullOrEmpty(Url)) From 36d6ce9ec8777cc49049fb814fdbfee1c05aa5f4 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:24:10 -0400 Subject: [PATCH 009/153] Unneeded build event (#2158) Build() at the end of the command creation isn't needed. The build is done on line 34. Co-authored-by: Cookiezzz --- .../application-commands/slash-commands/choice-slash-command.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md index 3951e1141..46805eb7f 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md +++ b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md @@ -27,7 +27,7 @@ private async Task Client_Ready() .AddChoice("Lovely", 4) .AddChoice("Excellent!", 5) .WithType(ApplicationCommandOptionType.Integer) - ).Build(); + ); try { From 5522bc443dbd7bc006730d1a16cf6bed88ddc525 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu, 3 Mar 2022 03:02:12 +0300 Subject: [PATCH 010/153] Create Complex Params Docs (#2160) * create complex params docs * Update docs/guides/int_framework/intro.md Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 15 ++++++++ .../samples/intro/complexparams.cs | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/guides/int_framework/samples/intro/complexparams.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 0a5cc19f1..764a100fe 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -143,6 +143,21 @@ In this case, user can only input Stage Channels and Text Channels to this param You can specify the permitted max/min value for a number type parameter using the [MaxValueAttribute] and [MinValueAttribute]. +#### Complex Parameters + +This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value. +Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ). +Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. +TypeConverter pattern is used to parse the constructor methods objects. + +[!code-csharp[Complex Parameter](samples/intro/usercommand.cs)] + +Interaction service complex parameter constructors are prioritized in the following order: + +1. Constructor matching the signature provided in the `[ComplexParameter(Type[])]` overload. +2. Constuctor tagged with `[ComplexParameterCtor]`. +3. Type's only public constuctor. + ## User Commands A valid User Command must have the following structure: diff --git a/docs/guides/int_framework/samples/intro/complexparams.cs b/docs/guides/int_framework/samples/intro/complexparams.cs new file mode 100644 index 000000000..72c0616cc --- /dev/null +++ b/docs/guides/int_framework/samples/intro/complexparams.cs @@ -0,0 +1,37 @@ +public class Vector3 +{ + public int X {get;} + public int Y {get;} + public int Z {get;} + + public Vector3() + { + X = 0; + Y = 0; + Z = 0; + } + + [ComplexParameterCtor] + public Vector3(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } +} + +// Both of the commands below are displayed to the users identically. + +// With complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector([ComplexParameter]Vector3 vector3) +{ + ... +} + +// Without complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector(int x, int y, int z) +{ + ... +} \ No newline at end of file From 50d0000e260e65fbbcd0af9f0487fcc86327c99e Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 2 Mar 2022 20:13:58 -0400 Subject: [PATCH 011/153] meta: 3.4.0 --- CHANGELOG.md | 23 +++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba61ed83..416f2ec6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [3.4.0] - 2022-3-2 + +## Added +- #2146 Add FromDateTimeOffset in TimestampTag (553055b) +- #2062 Add return statement to precondition handling (3e52fab) +- #2131 Add support for sending Message Flags (1fb62de) +- #2137 Add self_video to VoiceState (8bcd3da) +- #2151 Add Image property to Guild Scheduled Events (1dc473c) +- #2152 Add missing json error codes (202554f) +- #2153 Add IsInvitable and CreatedAt to threads (6bf5818) +- #2155 Add Interaction Service Complex Parameters (9ba64f6) +- #2156 Add Display name support for enum type converter (c800674) + +## Fixed +- #2117 Fix stream access exception when ratelimited (a1cfa41) +- #2128 Fix context menu comand message type (f601e9b) +- #2135 Fix NRE when ratelimmited requests don't return a body (b95b942) +- #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3) + +## Misc +- #2149 Clarify Users property on SocketGuildChannel (5594739) +- #2157 Enforce valid button styles (507a18d) + ## [3.3.2] - 2022-02-16 ### Fixed diff --git a/Discord.Net.targets b/Discord.Net.targets index 1b6a19c72..d0e17b3c5 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.3.2 + 3.4.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index c0821ce5d..2ad0164f4 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.3.2", + "_appFooter": "Discord.Net (c) 2015-2022 3.4.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index dec25413c..d98287ffa 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.3.2$suffix$ + 3.4.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 1ba96d6fbda0856f8b3c095a1e66a7e2b8c46aca Mon Sep 17 00:00:00 2001 From: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:30:17 -0500 Subject: [PATCH 012/153] Add configuration toggle to suppress Unknown dispatch warnings (#2162) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 +++- src/Discord.Net.WebSocket/DiscordSocketConfig.cs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index cd40a491f..b692f0691 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -78,6 +78,7 @@ namespace Discord.WebSocket internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; } + internal bool SuppressUnknownDispatchWarnings { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -150,6 +151,7 @@ namespace Discord.WebSocket AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; AlwaysResolveStickers = config.AlwaysResolveStickers; LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; + SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); @@ -2771,7 +2773,7 @@ namespace Discord.WebSocket #region Others default: - await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + if(!SuppressUnknownDispatchWarnings) await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); break; #endregion } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index f0e6dc857..4cd64dbc2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -188,6 +188,11 @@ namespace Discord.WebSocket /// public bool LogGatewayIntentWarnings { get; set; } = true; + /// + /// Gets or sets whether or not Unknown Dispatch event messages should be logged. + /// + public bool SuppressUnknownDispatchWarnings { get; set; } = true; + /// /// Initializes a new instance of the class with the default configuration. /// From 72629906541619bef58781f0e473c0d69f136e8b Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 3 Mar 2022 22:19:25 +0100 Subject: [PATCH 013/153] Resolve complex param sample reference (#2166) --- docs/guides/int_framework/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 764a100fe..abea2a735 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -150,7 +150,7 @@ Constructor methods support every attribute type that can be used with the regul Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. TypeConverter pattern is used to parse the constructor methods objects. -[!code-csharp[Complex Parameter](samples/intro/usercommand.cs)] +[!code-csharp[Complex Parameter](samples/intro/complexparams.cs)] Interaction service complex parameter constructors are prioritized in the following order: From 48bc723f9e7ca01e479115f89fff8ddc4c2135ea Mon Sep 17 00:00:00 2001 From: KeylAmi Date: Thu, 3 Mar 2022 16:20:02 -0500 Subject: [PATCH 014/153] Update bugreport.yml (#2159) * Update bugreport.yml * Update bugreport.yml removed d.net reference. fixed spelling. * Update bugreport.yml Adjusted verbiage for clarity --- .github/ISSUE_TEMPLATE/bugreport.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index d130991bf..e2c154130 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -76,3 +76,11 @@ body: ``` validations: required: false + - type: textarea + id: packages + attributes: + label: Packages + description: Please list all 3rd party packages in use if applicable, including their versions. + placeholder: Discord.Addons.Hosting V5.1.0, Discord.InteractivityAddon V2.4.0, etc. + validations: + required: true From a5d3add1d69618b4eb6ab9edb2ae2988b93ce231 Mon Sep 17 00:00:00 2001 From: Brendan McShane Date: Thu, 3 Mar 2022 16:20:34 -0500 Subject: [PATCH 015/153] Fix error with flag params. (#2165) --- src/Discord.Net.Rest/API/Rest/UploadFileParams.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 6340c3e38..67a690e4d 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -51,7 +51,7 @@ namespace Discord.API.Rest if (Stickers.IsSpecified) payload["sticker_ids"] = Stickers.Value; if (Flags.IsSpecified) - payload["flags"] = Flags; + payload["flags"] = Flags.Value; List attachments = new(); From cc6918d15721a7b06a206720ac761b5074a9e264 Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Wed, 9 Mar 2022 16:02:08 -0400 Subject: [PATCH 016/153] Add 10065 Error code (#2178) Co-authored-by: Discord.Net Robot --- src/Discord.Net.Core/DiscordErrorCode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 3eb9637eb..e9ed63e58 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -48,6 +48,7 @@ namespace Discord UnknownSticker = 10060, UnknownInteraction = 10062, UnknownApplicationCommand = 10063, + UnknownVoiceState = 10065, UnknownApplicationCommandPermissions = 10066, UnknownStageInstance = 10067, UnknownGuildMemberVerificationForm = 10068, From fb4250b88c14ad802079f25931739df170c1dc35 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 9 Mar 2022 23:10:00 +0300 Subject: [PATCH 017/153] Feature: Component TypeConverters and CustomID TypeReaders (#2169) * fix sharded client current user * add custom setter to group property of module builder * rename serilazation method * init * create typemap and default typereaders * add default readers * create typereader targetting flags * seperate custom id readers with component typeconverters * add typereaders * add customid readers * clean up component info argument parsing * remove obsolete method * add component typeconverters to modals * fix build errors * add inline docs * bug fixes * code cleanup and refactorings * fix build errors * add GenerateCustomIdString method to interaction service * add GenerateCustomIdString method to interaction service * add inline docs to componentparameterbuilder * add inline docs to GenerateCustomIdStringAsync method --- .../Commands/ComponentCommandBuilder.cs | 6 +- .../Modals/Inputs/IInputComponentBuilder.cs | 5 + .../Modals/Inputs/InputComponentBuilder.cs | 4 + .../Builders/Modals/ModalBuilder.cs | 6 +- .../Builders/ModuleClassBuilder.cs | 21 +- .../ComponentCommandParameterBuilder.cs | 77 ++++++ .../ModalCommandParameterBuilder.cs | 9 +- .../Entities/ITypeConverter.cs | 12 + .../Info/Commands/AutocompleteCommandInfo.cs | 9 +- .../Info/Commands/CommandInfo.cs | 35 ++- .../Info/Commands/ComponentCommandInfo.cs | 77 ++---- .../Info/Commands/ModalCommandInfo.cs | 36 ++- .../Info/Commands/SlashCommandInfo.cs | 63 ++--- .../InputComponents/InputComponentInfo.cs | 6 + .../Info/ModalInfo.cs | 51 +++- .../ComponentCommandParameterInfo.cs | 34 +++ .../Parameters/ModalCommandParameterInfo.cs | 9 +- .../InteractionService.cs | 233 ++++++++++++------ .../InteractionServiceConfig.cs | 2 +- src/Discord.Net.Interactions/Map/TypeMap.cs | 92 +++++++ .../ComponentTypeConverter.cs | 39 +++ .../DefaultArrayComponentConverter.cs | 45 ++++ .../DefaultValueComponentConverter.cs | 26 ++ .../DefaultEntityTypeConverter.cs | 0 .../DefaultValueConverter.cs | 0 .../{ => SlashCommands}/EnumConverter.cs | 0 .../{ => SlashCommands}/NullableConverter.cs | 0 .../{ => SlashCommands}/TimeSpanConverter.cs | 0 .../{ => SlashCommands}/TypeConverter.cs | 2 +- .../TypeReaders/DefaultSnowflakeReader.cs | 48 ++++ .../TypeReaders/DefaultValueReader.cs | 22 ++ .../TypeReaders/EnumReader.cs | 25 ++ .../TypeReaders/TypeReader.cs | 46 ++++ .../Utilities/ModalUtils.cs | 10 +- 34 files changed, 812 insertions(+), 238 deletions(-) create mode 100644 src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs create mode 100644 src/Discord.Net.Interactions/Entities/ITypeConverter.cs create mode 100644 src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs create mode 100644 src/Discord.Net.Interactions/Map/TypeMap.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/DefaultEntityTypeConverter.cs (100%) rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/DefaultValueConverter.cs (100%) rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/EnumConverter.cs (100%) rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/NullableConverter.cs (100%) rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/TimeSpanConverter.cs (100%) rename src/Discord.Net.Interactions/TypeConverters/{ => SlashCommands}/TypeConverter.cs (95%) create mode 100644 src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs create mode 100644 src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs create mode 100644 src/Discord.Net.Interactions/TypeReaders/EnumReader.cs create mode 100644 src/Discord.Net.Interactions/TypeReaders/TypeReader.cs diff --git a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs index e42dfabce..dd857498c 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ComponentCommandBuilder.cs @@ -5,7 +5,7 @@ namespace Discord.Interactions.Builders /// /// Represents a builder for creating . /// - public sealed class ComponentCommandBuilder : CommandBuilder + public sealed class ComponentCommandBuilder : CommandBuilder { protected override ComponentCommandBuilder Instance => this; @@ -26,9 +26,9 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// - public override ComponentCommandBuilder AddParameter (Action configure) + public override ComponentCommandBuilder AddParameter (Action configure) { - var parameter = new CommandParameterBuilder(this); + var parameter = new ComponentCommandParameterBuilder(this); configure(parameter); AddParameters(parameter); return this; diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs index 37cd861c4..ad2f07c73 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs @@ -38,6 +38,11 @@ namespace Discord.Interactions.Builders /// Type Type { get; } + /// + /// Get the assigned to this input. + /// + ComponentTypeConverter TypeConverter { get; } + /// /// Gets the default value of this input component. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs index c2b9b0645..7d1d96712 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs @@ -33,6 +33,9 @@ namespace Discord.Interactions.Builders /// public Type Type { get; private set; } + /// + public ComponentTypeConverter TypeConverter { get; private set; } + /// public object DefaultValue { get; set; } @@ -111,6 +114,7 @@ namespace Discord.Interactions.Builders public TBuilder WithType(Type type) { Type = type; + TypeConverter = Modal._interactionService.GetComponentTypeConverter(type); return Instance; } diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index e120e78be..fc1dbdc0e 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -9,6 +9,7 @@ namespace Discord.Interactions.Builders /// public class ModalBuilder { + internal readonly InteractionService _interactionService; internal readonly List _components; /// @@ -31,11 +32,12 @@ namespace Discord.Interactions.Builders /// public IReadOnlyCollection Components => _components; - internal ModalBuilder(Type type) + internal ModalBuilder(Type type, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(type)) throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); + _interactionService = interactionService; _components = new(); } @@ -43,7 +45,7 @@ namespace Discord.Interactions.Builders /// Initializes a new /// /// The initialization delegate for this modal. - public ModalBuilder(Type type, ModalInitializer modalInitializer) : this(type) + public ModalBuilder(Type type, ModalInitializer modalInitializer, InteractionService interactionService) : this(type, interactionService) { ModalInitializer = modalInitializer; } diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 88a34f3b2..b2317d1f3 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -231,9 +231,6 @@ namespace Discord.Interactions.Builders private static void BuildComponentCommand (ComponentCommandBuilder builder, Func createInstance, MethodInfo methodInfo, InteractionService commandService, IServiceProvider services) { - if (!methodInfo.GetParameters().All(x => x.ParameterType == typeof(string) || x.ParameterType == typeof(string[]))) - throw new InvalidOperationException($"Interaction method parameters all must be types of {typeof(string).Name} or {typeof(string[]).Name}"); - var attributes = methodInfo.GetCustomAttributes(); builder.MethodName = methodInfo.Name; @@ -260,8 +257,10 @@ namespace Discord.Interactions.Builders var parameters = methodInfo.GetParameters(); + var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; + foreach (var parameter in parameters) - builder.AddParameter(x => BuildParameter(x, parameter)); + builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); builder.Callback = CreateCallback(createInstance, methodInfo, commandService); } @@ -310,8 +309,8 @@ namespace Discord.Interactions.Builders if (parameters.Count(x => typeof(IModal).IsAssignableFrom(x.ParameterType)) > 1) throw new InvalidOperationException($"A modal command can only have one {nameof(IModal)} parameter."); - if (!parameters.All(x => x.ParameterType == typeof(string) || typeof(IModal).IsAssignableFrom(x.ParameterType))) - throw new InvalidOperationException($"All parameters of a modal command must be either a string or an implementation of {nameof(IModal)}"); + if (!typeof(IModal).IsAssignableFrom(parameters.Last().ParameterType)) + throw new InvalidOperationException($"Last parameter of a modal command must be an implementation of {nameof(IModal)}"); var attributes = methodInfo.GetCustomAttributes(); @@ -464,6 +463,12 @@ namespace Discord.Interactions.Builders builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); } + private static void BuildComponentParameter(ComponentCommandParameterBuilder builder, ParameterInfo paramInfo, bool isComponentParam) + { + builder.SetIsRouteSegment(!isComponentParam); + BuildParameter(builder, paramInfo); + } + private static void BuildParameter (ParameterBuilder builder, ParameterInfo paramInfo) where TInfo : class, IParameterInfo where TBuilder : ParameterBuilder @@ -495,7 +500,7 @@ namespace Discord.Interactions.Builders #endregion #region Modals - public static ModalInfo BuildModalInfo(Type modalType) + public static ModalInfo BuildModalInfo(Type modalType, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(modalType)) throw new InvalidOperationException($"{modalType.FullName} isn't an implementation of {typeof(IModal).FullName}"); @@ -504,7 +509,7 @@ namespace Discord.Interactions.Builders try { - var builder = new ModalBuilder(modalType) + var builder = new ModalBuilder(modalType, interactionService) { Title = instance.Title }; diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs new file mode 100644 index 000000000..d9f1463c3 --- /dev/null +++ b/src/Discord.Net.Interactions/Builders/Parameters/ComponentCommandParameterBuilder.cs @@ -0,0 +1,77 @@ +using System; + +namespace Discord.Interactions.Builders +{ + /// + /// Represents a builder for creating . + /// + public class ComponentCommandParameterBuilder : ParameterBuilder + { + /// + /// Get the assigned to this parameter, if is . + /// + public ComponentTypeConverter TypeConverter { get; private set; } + + /// + /// Get the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; private set; } + + /// + /// Gets whether this parameter is a CustomId segment or a Component value parameter. + /// + public bool IsRouteSegmentParameter { get; private set; } + + /// + protected override ComponentCommandParameterBuilder Instance => this; + + internal ComponentCommandParameterBuilder(ICommandBuilder command) : base(command) { } + + /// + /// Initializes a new . + /// + /// Parent command of this parameter. + /// Name of this command. + /// Type of this parameter. + public ComponentCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + + /// + public override ComponentCommandParameterBuilder SetParameterType(Type type) => SetParameterType(type, null); + + /// + /// Sets . + /// + /// New value of the . + /// Service container to be used to resolve the dependencies of this parameters . + /// + /// The builder instance. + /// + public ComponentCommandParameterBuilder SetParameterType(Type type, IServiceProvider services) + { + base.SetParameterType(type); + + if (IsRouteSegmentParameter) + TypeReader = Command.Module.InteractionService.GetTypeReader(type); + else + TypeConverter = Command.Module.InteractionService.GetComponentTypeConverter(ParameterType, services); + + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ComponentCommandParameterBuilder SetIsRouteSegment(bool isRouteSegment) + { + IsRouteSegmentParameter = isRouteSegment; + return this; + } + + internal override ComponentCommandParameterInfo Build(ICommandInfo command) + => new(this, command); + } +} diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs index a0315e1ea..8cb9b3ab9 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ModalCommandParameterBuilder.cs @@ -20,6 +20,11 @@ namespace Discord.Interactions.Builders /// public bool IsModalParameter => Modal is not null; + /// + /// Gets the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; private set; } + internal ModalCommandParameterBuilder(ICommandBuilder command) : base(command) { } /// @@ -34,7 +39,9 @@ namespace Discord.Interactions.Builders public override ModalCommandParameterBuilder SetParameterType(Type type) { if (typeof(IModal).IsAssignableFrom(type)) - Modal = ModalUtils.GetOrAdd(type); + Modal = ModalUtils.GetOrAdd(type, Command.Module.InteractionService); + else + TypeReader = Command.Module.InteractionService.GetTypeReader(type); return base.SetParameterType(type); } diff --git a/src/Discord.Net.Interactions/Entities/ITypeConverter.cs b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs new file mode 100644 index 000000000..c692b29cb --- /dev/null +++ b/src/Discord.Net.Interactions/Entities/ITypeConverter.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal interface ITypeConverter + { + public bool CanConvertTo(Type type); + + public Task ReadAsync(IInteractionContext context, T option, IServiceProvider services); + } +} diff --git a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs index 712b058a3..9e30c55f4 100644 --- a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs @@ -41,14 +41,7 @@ namespace Discord.Interactions if (context.Interaction is not IAutocompleteInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); - try - { - return await RunAsync(context, Array.Empty(), services).ConfigureAwait(false); - } - catch (Exception ex) - { - return ExecuteResult.FromError(ex); - } + return await RunAsync(context, Array.Empty(), services).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index 49ad009c9..ea5ded11c 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -123,10 +123,7 @@ namespace Discord.Interactions return moduleResult; var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); - if (!commandResult.IsSuccess) - return commandResult; - - return PreconditionResult.FromSuccess(); + return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); } protected async Task RunAsync(IInteractionContext context, object[] args, IServiceProvider services) @@ -140,8 +137,8 @@ namespace Discord.Interactions using var scope = services?.CreateScope(); return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); } - else - return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); + + return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); } case RunMode.Async: _ = Task.Run(async () => @@ -170,20 +167,14 @@ namespace Discord.Interactions { var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); if (!preconditionResult.IsSuccess) - { - await InvokeModuleEvent(context, preconditionResult).ConfigureAwait(false); - return preconditionResult; - } + return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); var index = 0; foreach (var parameter in Parameters) { var result = await parameter.CheckPreconditionsAsync(context, args[index++], services).ConfigureAwait(false); if (!result.IsSuccess) - { - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; - } + return await InvokeEventAndReturn(context, result).ConfigureAwait(false); } var task = _action(context, args, services, this); @@ -192,20 +183,16 @@ namespace Discord.Interactions { var result = await resultTask.ConfigureAwait(false); await InvokeModuleEvent(context, result).ConfigureAwait(false); - if (result is RuntimeResult || result is ExecuteResult) + if (result is RuntimeResult or ExecuteResult) return result; } else { await task.ConfigureAwait(false); - var result = ExecuteResult.FromSuccess(); - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; + return await InvokeEventAndReturn(context, ExecuteResult.FromSuccess()).ConfigureAwait(false); } - var failResult = ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason"); - await InvokeModuleEvent(context, failResult).ConfigureAwait(false); - return failResult; + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.Unsuccessful, "Command execution failed for an unknown reason")).ConfigureAwait(false); } catch (Exception ex) { @@ -234,6 +221,12 @@ namespace Discord.Interactions } } + protected async ValueTask InvokeEventAndReturn(IInteractionContext context, IResult result) + { + await InvokeModuleEvent(context, result).ConfigureAwait(false); + return result; + } + private static bool CheckTopLevel(ModuleInfo parent) { var currentParent = parent; diff --git a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs index 0e43af3a8..22d6aba6c 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs @@ -1,5 +1,4 @@ using Discord.Interactions.Builders; -using Discord.WebSocket; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -11,10 +10,10 @@ namespace Discord.Interactions /// /// Represents the info class of an attribute based method for handling Component Interaction events. /// - public class ComponentCommandInfo : CommandInfo + public class ComponentCommandInfo : CommandInfo { /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyCollection Parameters { get; } /// public override bool SupportsWildCards => true; @@ -42,80 +41,46 @@ namespace Discord.Interactions if (context.Interaction is not IComponentInteraction componentInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); - var args = new List(); - - if (additionalArgs is not null) - args.AddRange(additionalArgs); - - if (componentInteraction.Data?.Values is not null) - args.AddRange(componentInteraction.Data.Values); - - return await ExecuteAsync(context, Parameters, args, services); + return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services); } /// - public async Task ExecuteAsync(IInteractionContext context, IEnumerable paramList, IEnumerable values, + public async Task ExecuteAsync(IInteractionContext context, IEnumerable paramList, IEnumerable wildcardCaptures, IComponentInteractionData data, IServiceProvider services) { + var paramCount = paramList.Count(); + var captureCount = wildcardCaptures?.Count() ?? 0; + if (context.Interaction is not IComponentInteraction messageComponent) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); try { - var strCount = Parameters.Count(x => x.ParameterType == typeof(string)); + var args = new object[paramCount]; + + for (var i = 0; i < paramCount; i++) + { + var parameter = Parameters.ElementAt(i); + var isCapture = i < captureCount; - if (strCount > values?.Count()) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); + if (isCapture ^ parameter.IsRouteSegmentParameter) + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); - var componentValues = messageComponent.Data?.Values; + var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : + await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); - var args = new object[Parameters.Count]; + if (!readResult.IsSuccess) + return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); - if (componentValues is not null) - { - if (Parameters.Last().ParameterType == typeof(string[])) - args[args.Length - 1] = componentValues.ToArray(); - else - return ExecuteResult.FromError(InteractionCommandError.BadArgs, $"Select Menu Interaction handlers must accept a {typeof(string[]).FullName} as its last parameter"); + args[i] = readResult.Value; } - for (var i = 0; i < strCount; i++) - args[i] = values.ElementAt(i); - return await RunAsync(context, args, services).ConfigureAwait(false); } catch (Exception ex) { - return ExecuteResult.FromError(ex); - } - } - - private static object[] GenerateArgs(IEnumerable paramList, IEnumerable argList) - { - var result = new object[paramList.Count()]; - - for (var i = 0; i < paramList.Count(); i++) - { - var parameter = paramList.ElementAt(i); - - if (argList?.ElementAt(i) == null) - { - if (!parameter.IsRequired) - result[i] = parameter.DefaultValue; - else - throw new InvalidOperationException($"Component Interaction handler is executed with too few args."); - } - else if (parameter.IsParameterArray) - { - string[] paramArray = new string[argList.Count() - i]; - argList.ToArray().CopyTo(paramArray, i); - result[i] = paramArray; - } - else - result[i] = argList?.ElementAt(i); + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); } - - return result; } protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs index a750603fc..a55a1307a 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.Tracing; using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -47,21 +48,38 @@ namespace Discord.Interactions try { - var args = new List(); + var args = new object[Parameters.Count]; + var captureCount = additionalArgs.Length; - if (additionalArgs is not null) - args.AddRange(additionalArgs); + for(var i = 0; i < Parameters.Count; i++) + { + var parameter = Parameters.ElementAt(i); - var modal = Modal.CreateModal(modalInteraction, Module.CommandService._exitOnMissingModalField); - args.Add(modal); + if(i < captureCount) + { + var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); + if (!readResult.IsSuccess) + return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); - return await RunAsync(context, args.ToArray(), services); + args[i] = readResult.Value; + } + else + { + var modalResult = await Modal.CreateModalAsync(context, services, Module.CommandService._exitOnMissingModalField).ConfigureAwait(false); + if (!modalResult.IsSuccess) + return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); + + if (modalResult is not ParseResult parseResult) + return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); + + args[i] = parseResult.Value; + } + } + return await RunAsync(context, args, services); } catch (Exception ex) { - var result = ExecuteResult.FromError(ex); - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index 456ad4bfe..a123ac183 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -70,34 +70,27 @@ namespace Discord.Interactions { try { - var args = new object[paramList.Count()]; + var slashCommandParameterInfos = paramList.ToList(); + var args = new object[slashCommandParameterInfos.Count]; - for (var i = 0; i < paramList.Count(); i++) + for (var i = 0; i < slashCommandParameterInfos.Count; i++) { - var parameter = paramList.ElementAt(i); - + var parameter = slashCommandParameterInfos[i]; var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); - if(!result.IsSuccess) - { - var execResult = ExecuteResult.FromError(result); - await InvokeModuleEvent(context, execResult).ConfigureAwait(false); - return execResult; - } + if (!result.IsSuccess) + return await InvokeEventAndReturn(context, result).ConfigureAwait(false); - if (result is ParseResult parseResult) - args[i] = parseResult.Value; - else + if (result is not ParseResult parseResult) return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); - } + args[i] = parseResult.Value; + } return await RunAsync(context, args, services).ConfigureAwait(false); } - catch (Exception ex) + catch(Exception ex) { - var result = ExecuteResult.FromError(ex); - await InvokeModuleEvent(context, result).ConfigureAwait(false); - return result; + return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); } } @@ -115,37 +108,27 @@ namespace Discord.Interactions if (!result.IsSuccess) return result; - if (result is ParseResult parseResult) - ctorArgs[i] = parseResult.Value; - else + if (result is not ParseResult parseResult) return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); + + ctorArgs[i] = parseResult.Value; } return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); } - else - { - var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); - if (arg == default) - { - if (parameterInfo.IsRequired) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); - else - return ParseResult.FromSuccess(parameterInfo.DefaultValue); - } - else - { - var typeConverter = parameterInfo.TypeConverter; + var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); - var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + if (arg == default) + return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : + ParseResult.FromSuccess(parameterInfo.DefaultValue); - if (!readResult.IsSuccess) - return readResult; + var typeConverter = parameterInfo.TypeConverter; + var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + if (!readResult.IsSuccess) + return readResult; - return ParseResult.FromSuccess(readResult.Value); - } - } + return ParseResult.FromSuccess(readResult.Value); } protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) diff --git a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs index 790838ad9..05695f862 100644 --- a/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/InputComponents/InputComponentInfo.cs @@ -39,6 +39,11 @@ namespace Discord.Interactions /// public Type Type { get; } + /// + /// Gets the assigned to this component. + /// + public ComponentTypeConverter TypeConverter { get; } + /// /// Gets the default value of this component. /// @@ -57,6 +62,7 @@ namespace Discord.Interactions IsRequired = builder.IsRequired; ComponentType = builder.ComponentType; Type = builder.Type; + TypeConverter = builder.TypeConverter; DefaultValue = builder.DefaultValue; Attributes = builder.Attributes.ToImmutableArray(); } diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index edc31373e..5130c26a1 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; namespace Discord.Interactions { @@ -19,6 +20,7 @@ namespace Discord.Interactions /// public class ModalInfo { + internal readonly InteractionService _interactionService; internal readonly ModalInitializer _initializer; /// @@ -53,16 +55,18 @@ namespace Discord.Interactions TextComponents = Components.OfType().ToImmutableArray(); + _interactionService = builder._interactionService; _initializer = builder.ModalInitializer; } /// /// Creates an and fills it with provided message components. /// - /// that will be injected into the modal. + /// that will be injected into the modal. /// /// A filled with the provided components. /// + [Obsolete("This method is no longer supported with the introduction of Component TypeConverters, please use the CreateModalAsync method.")] public IModal CreateModal(IModalInteraction modalInteraction, bool throwOnMissingField = false) { var args = new object[Components.Count]; @@ -86,5 +90,50 @@ namespace Discord.Interactions return _initializer(args); } + + /// + /// Creates an and fills it with provided message components. + /// + /// Context of the that will be injected into the modal. + /// Services to be passed onto the s of the modal fiels. + /// Wheter or not this method should exit on encountering a missing modal field. + /// + /// A if a type conversion has failed, else a . + /// + public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) + { + if (context.Interaction is not IModalInteraction modalInteraction) + return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + + services ??= EmptyServiceProvider.Instance; + + var args = new object[Components.Count]; + var components = modalInteraction.Data.Components.ToList(); + + for (var i = 0; i < Components.Count; i++) + { + var input = Components.ElementAt(i); + var component = components.Find(x => x.CustomId == input.CustomId); + + if (component is null) + { + if (!throwOnMissingField) + args[i] = input.DefaultValue; + else + return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); + } + else + { + var readResult = await input.TypeConverter.ReadAsync(context, component, services).ConfigureAwait(false); + + if (!readResult.IsSuccess) + return readResult; + + args[i] = readResult.Value; + } + } + + return ParseResult.FromSuccess(_initializer(args)); + } } } diff --git a/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs new file mode 100644 index 000000000..36b75ddb7 --- /dev/null +++ b/src/Discord.Net.Interactions/Info/Parameters/ComponentCommandParameterInfo.cs @@ -0,0 +1,34 @@ +using Discord.Interactions.Builders; + +namespace Discord.Interactions +{ + /// + /// Represents the parameter info class for commands. + /// + public class ComponentCommandParameterInfo : CommandParameterInfo + { + /// + /// Gets the that will be used to convert a message component value into + /// , if is false. + /// + public ComponentTypeConverter TypeConverter { get; } + + /// + /// Gets the that will be used to convert a CustomId segment value into + /// , if is . + /// + public TypeReader TypeReader { get; } + + /// + /// Gets whether this parameter is a CustomId segment or a component value parameter. + /// + public bool IsRouteSegmentParameter { get; } + + internal ComponentCommandParameterInfo(ComponentCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) + { + TypeConverter = builder.TypeConverter; + TypeReader = builder.TypeReader; + IsRouteSegmentParameter = builder.IsRouteSegmentParameter; + } + } +} diff --git a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs index 28162e109..cafb0b7f5 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/ModalCommandParameterInfo.cs @@ -15,7 +15,12 @@ namespace Discord.Interactions /// /// Gets whether this parameter is an /// - public bool IsModalParameter => Modal is not null; + public bool IsModalParameter { get; } + + /// + /// Gets the assigned to this parameter, if is . + /// + public TypeReader TypeReader { get; } /// public new ModalCommandInfo Command => base.Command as ModalCommandInfo; @@ -23,6 +28,8 @@ namespace Discord.Interactions internal ModalCommandParameterInfo(ModalCommandParameterBuilder builder, ICommandInfo command) : base(builder, command) { Modal = builder.Modal; + IsModalParameter = builder.IsModalParameter; + TypeReader = builder.TypeReader; } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index bf56eddc5..927e39735 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -3,6 +3,7 @@ using Discord.Logging; using Discord.Rest; using Discord.WebSocket; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -66,8 +67,9 @@ namespace Discord.Interactions private readonly CommandMap _autocompleteCommandMap; private readonly CommandMap _modalCommandMap; private readonly HashSet _moduleDefs; - private readonly ConcurrentDictionary _typeConverters; - private readonly ConcurrentDictionary _genericTypeConverters; + private readonly TypeMap _typeConverterMap; + private readonly TypeMap _compTypeConverterMap; + private readonly TypeMap _typeReaderMap; private readonly ConcurrentDictionary _autocompleteHandlers = new(); private readonly ConcurrentDictionary _modalInfos = new(); private readonly SemaphoreSlim _lock; @@ -179,22 +181,38 @@ namespace Discord.Interactions _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; - _genericTypeConverters = new ConcurrentDictionary - { - [typeof(IChannel)] = typeof(DefaultChannelConverter<>), - [typeof(IRole)] = typeof(DefaultRoleConverter<>), - [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), - [typeof(IUser)] = typeof(DefaultUserConverter<>), - [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueConverter<>), - [typeof(Enum)] = typeof(EnumConverter<>), - [typeof(Nullable<>)] = typeof(NullableConverter<>), - }; + _typeConverterMap = new TypeMap(this, new ConcurrentDictionary + { + [typeof(TimeSpan)] = new TimeSpanConverter() + }, new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelConverter<>), + [typeof(IRole)] = typeof(DefaultRoleConverter<>), + [typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>), + [typeof(IUser)] = typeof(DefaultUserConverter<>), + [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueConverter<>), + [typeof(Enum)] = typeof(EnumConverter<>), + [typeof(Nullable<>)] = typeof(NullableConverter<>) + }); + + _compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) + }); - _typeConverters = new ConcurrentDictionary - { - [typeof(TimeSpan)] = new TimeSpanConverter() - }; + _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), + new ConcurrentDictionary + { + [typeof(IChannel)] = typeof(DefaultChannelReader<>), + [typeof(IRole)] = typeof(DefaultRoleReader<>), + [typeof(IUser)] = typeof(DefaultUserReader<>), + [typeof(IMessage)] = typeof(DefaultMessageReader<>), + [typeof(IConvertible)] = typeof(DefaultValueReader<>), + [typeof(Enum)] = typeof(EnumReader<>) + }); } /// @@ -293,7 +311,7 @@ namespace Discord.Interactions public async Task AddModuleAsync (Type type, IServiceProvider services) { if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) - throw new ArgumentException("Type parameter must be a type of Slash Module", "T"); + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(type)); services ??= EmptyServiceProvider.Instance; @@ -326,7 +344,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from and to a guild. + /// Register Application Commands from and to a guild. /// /// Id of the target guild. /// If , this operation will not delete the commands that are missing from . @@ -422,7 +440,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from modules provided in to a guild. + /// Register Application Commands from modules provided in to a guild. /// /// The target guild. /// Modules to be registered to Discord. @@ -449,7 +467,7 @@ namespace Discord.Interactions } /// - /// Register Application Commands from modules provided in as global commands. + /// Register Application Commands from modules provided in as global commands. /// /// Modules to be registered to Discord. /// @@ -677,7 +695,7 @@ namespace Discord.Interactions public async Task ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) { var interaction = context.Interaction; - + return interaction switch { ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), @@ -781,47 +799,24 @@ namespace Discord.Interactions return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } - internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null) - { - if (_typeConverters.TryGetValue(type, out var specific)) - return specific; - else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type) - || (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))) - { - services ??= EmptyServiceProvider.Instance; - - var converterType = GetMostSpecificTypeConverter(type); - var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services); - _typeConverters[type] = converter; - return converter; - } - - else if (_typeConverters.Any(x => x.Value.CanConvertTo(type))) - return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value; - - throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); - } + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) + => _typeConverterMap.Get(type, services); /// /// Add a concrete type . /// /// Primary target of the . /// The instance. - public void AddTypeConverter (TypeConverter converter) => - AddTypeConverter(typeof(T), converter); + public void AddTypeConverter(TypeConverter converter) => + _typeConverterMap.AddConcrete(converter); /// /// Add a concrete type . /// /// Primary target of the . /// The instance. - public void AddTypeConverter (Type type, TypeConverter converter) - { - if (!converter.CanConvertTo(type)) - throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); - - _typeConverters[type] = converter; - } + public void AddTypeConverter(Type type, TypeConverter converter) => + _typeConverterMap.AddConcrete(type, converter); /// /// Add a generic type . @@ -829,30 +824,121 @@ namespace Discord.Interactions /// Generic Type constraint of the of the . /// Type of the . - public void AddGenericTypeConverter (Type converterType) => - AddGenericTypeConverter(typeof(T), converterType); + public void AddGenericTypeConverter(Type converterType) => + _typeConverterMap.AddGeneric(converterType); /// /// Add a generic type . /// /// Generic Type constraint of the of the . /// Type of the . - public void AddGenericTypeConverter (Type targetType, Type converterType) - { - if (!converterType.IsGenericTypeDefinition) - throw new ArgumentException($"{converterType.FullName} is not generic."); + public void AddGenericTypeConverter(Type targetType, Type converterType) => + _typeConverterMap.AddGeneric(targetType, converterType); + + internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) => + _compTypeConverterMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(ComponentTypeConverter converter) => + AddComponentTypeConverter(typeof(T), converter); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) => + _compTypeConverterMap.AddConcrete(type, converter); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type converterType) => + AddGenericComponentTypeConverter(typeof(T), converterType); + + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericComponentTypeConverter(Type targetType, Type converterType) => + _compTypeConverterMap.AddGeneric(targetType, converterType); + + internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) => + _typeReaderMap.Get(type, services); + + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(TypeReader reader) => + AddTypeReader(typeof(T), reader); - var genericArguments = converterType.GetGenericArguments(); + /// + /// Add a concrete type . + /// + /// Primary target of the . + /// The instance. + public void AddTypeReader(Type type, TypeReader reader) => + _typeReaderMap.AddConcrete(type, reader); - if (genericArguments.Count() > 1) - throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type readerType) => + AddGenericTypeReader(typeof(T), readerType); - var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); + /// + /// Add a generic type . + /// + /// Generic Type constraint of the of the . + /// Type of the . + public void AddGenericTypeReader(Type targetType, Type readerType) => + _typeReaderMap.AddGeneric(targetType, readerType); - if (!constraints.Any(x => x.IsAssignableFrom(targetType))) - throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); + /// + /// Serialize an object using a into a to be placed in a Component CustomId. + /// + /// Type of the object to be serialized. + /// Object to be serialized. + /// Services that will be passed on to the . + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public Task SerializeValueAsync(T obj, IServiceProvider services) => + _typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services); - _genericTypeConverters[targetType] = converterType; + /// + /// Serialize and format multiple objects into a Custom Id string. + /// + /// A composite format string. + /// >Services that will be passed on to the s. + /// Objects to be serialized. + /// + /// A task representing the conversion process. The task result contains the result of the conversion. + /// + public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args) + { + var serializedValues = new string[args.Length]; + + for(var i = 0; i < args.Length; i++) + { + var arg = args[i]; + var typeReader = _typeReaderMap.Get(arg.GetType(), null); + var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false); + serializedValues[i] = result; + } + + return string.Format(format, serializedValues); } /// @@ -870,7 +956,7 @@ namespace Discord.Interactions if (_modalInfos.ContainsKey(type)) throw new InvalidOperationException($"Modal type {type.FullName} already exists."); - return ModalUtils.GetOrAdd(type); + return ModalUtils.GetOrAdd(type, this); } internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) @@ -1016,7 +1102,7 @@ namespace Discord.Interactions public ModuleInfo GetModuleInfo ( ) where TModule : class { if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) - throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule"); + throw new ArgumentException("Type parameter must be a type of Slash Module", nameof(TModule)); var module = _typedModuleDefs[typeof(TModule)]; @@ -1032,21 +1118,6 @@ namespace Discord.Interactions _lock.Dispose(); } - private Type GetMostSpecificTypeConverter (Type type) - { - if (_genericTypeConverters.TryGetValue(type, out var matching)) - return matching; - - if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) - return genericDefinition; - - var typeInterfaces = type.GetInterfaces(); - var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type)) - .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); - - return candidates.First().Value; - } - private void EnsureClientReady() { if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index 136cba24c..b6576a49f 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -31,7 +31,7 @@ namespace Discord.Interactions /// /// Gets or sets the string expression that will be treated as a wild card. /// - public string WildCardExpression { get; set; } + public string WildCardExpression { get; set; } = "*"; /// /// Gets or sets the option to use compiled lambda expressions to create module instances and execute commands. This method improves performance at the cost of memory. diff --git a/src/Discord.Net.Interactions/Map/TypeMap.cs b/src/Discord.Net.Interactions/Map/TypeMap.cs new file mode 100644 index 000000000..ef1ef4a53 --- /dev/null +++ b/src/Discord.Net.Interactions/Map/TypeMap.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Discord.Interactions +{ + internal class TypeMap + where TConverter : class, ITypeConverter + { + private readonly ConcurrentDictionary _concretes; + private readonly ConcurrentDictionary _generics; + private readonly InteractionService _interactionService; + + public TypeMap(InteractionService interactionService, IDictionary concretes = null, IDictionary generics = null) + { + _interactionService = interactionService; + _concretes = concretes is not null ? new(concretes) : new(); + _generics = generics is not null ? new(generics) : new(); + } + + internal TConverter Get(Type type, IServiceProvider services = null) + { + if (_concretes.TryGetValue(type, out var specific)) + return specific; + + if (_generics.Any(x => x.Key.IsAssignableFrom(type) + || x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())) + { + services ??= EmptyServiceProvider.Instance; + + var converterType = GetMostSpecific(type); + var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), _interactionService, services); + _concretes[type] = converter; + return converter; + } + + if (_concretes.Any(x => x.Value.CanConvertTo(type))) + return _concretes.First(x => x.Value.CanConvertTo(type)).Value; + + throw new ArgumentException($"No type {typeof(TConverter).Name} is defined for this {type.FullName}", nameof(type)); + } + + public void AddConcrete(TConverter converter) => + AddConcrete(typeof(TTarget), converter); + + public void AddConcrete(Type type, TConverter converter) + { + if (!converter.CanConvertTo(type)) + throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); + + _concretes[type] = converter; + } + + public void AddGeneric(Type converterType) => + AddGeneric(typeof(TTarget), converterType); + + public void AddGeneric(Type targetType, Type converterType) + { + if (!converterType.IsGenericTypeDefinition) + throw new ArgumentException($"{converterType.FullName} is not generic."); + + var genericArguments = converterType.GetGenericArguments(); + + if (genericArguments.Length > 1) + throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); + + var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); + + if (!constraints.Any(x => x.IsAssignableFrom(targetType))) + throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); + + _generics[targetType] = converterType; + } + + private Type GetMostSpecific(Type type) + { + if (_generics.TryGetValue(type, out var matching)) + return matching; + + if (type.IsGenericType && _generics.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) + return genericDefinition; + + var typeInterfaces = type.GetInterfaces(); + var candidates = _generics.Where(x => x.Key.IsAssignableFrom(type)) + .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); + + return candidates.First().Value; + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs new file mode 100644 index 000000000..e406d4a26 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/ComponentTypeConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Base class for creating Component TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// + public abstract class ComponentTypeConverter : ITypeConverter + { + /// + /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// + public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command exexution context. + /// Recieved option payload. + /// Service provider that will be used to initialize the command module. + /// + /// The result of the read process. + /// + public abstract Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services); + } + + /// + public abstract class ComponentTypeConverter : ComponentTypeConverter + { + /// + public sealed override bool CanConvertTo(Type type) => + typeof(T).IsAssignableFrom(type); + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs new file mode 100644 index 000000000..87fc431c5 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultArrayComponentConverter : ComponentTypeConverter + { + private readonly TypeReader _typeReader; + private readonly Type _underlyingType; + + public DefaultArrayComponentConverter(InteractionService interactionService) + { + var type = typeof(T); + + if (!type.IsArray) + throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type."); + + _underlyingType = typeof(T).GetElementType(); + _typeReader = interactionService.GetTypeReader(_underlyingType); + } + + public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + var results = new List(); + + foreach (var value in option.Values) + { + var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + results.Add(result); + } + + var destination = Array.CreateInstance(_underlyingType, results.Count); + + for (var i = 0; i < results.Count; i++) + destination.SetValue(results[i].Value, i); + + return TypeConverterResult.FromSuccess(destination); + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs new file mode 100644 index 000000000..9ed82c6ed --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultValueComponentConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultValueComponentConverter : ComponentTypeConverter + where T : IConvertible + { + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + { + try + { + return option.Type switch + { + ComponentType.SelectMenu => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(string.Join(",", option.Values), typeof(T)))), + ComponentType.TextInput => Task.FromResult(TypeConverterResult.FromSuccess(Convert.ChangeType(option.Value, typeof(T)))), + _ => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option.Type} doesn't have a convertible value.")) + }; + } + catch (InvalidCastException castEx) + { + return Task.FromResult(TypeConverterResult.FromError(castEx)); + } + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/DefaultEntityTypeConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultEntityTypeConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/DefaultValueConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/DefaultValueConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/EnumConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/NullableConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs similarity index 100% rename from src/Discord.Net.Interactions/TypeConverters/TimeSpanConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TimeSpanConverter.cs diff --git a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs similarity index 95% rename from src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs rename to src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs index 360b6ce4a..09cbc56d4 100644 --- a/src/Discord.Net.Interactions/TypeConverters/TypeConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/TypeConverter.cs @@ -6,7 +6,7 @@ namespace Discord.Interactions /// /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters. /// - public abstract class TypeConverter + public abstract class TypeConverter : ITypeConverter { /// /// Will be used to search for alternative TypeConverters whenever the Command Service encounters an unknown parameter type. diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs new file mode 100644 index 000000000..e2ac1efbd --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/DefaultSnowflakeReader.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal abstract class DefaultSnowflakeReader : TypeReader + where T : class, ISnowflakeEntity + { + protected abstract Task GetEntity(ulong id, IInteractionContext ctx); + + public override async Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + if (!ulong.TryParse(option, out var snowflake)) + return TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} isn't a valid snowflake thus cannot be converted into {typeof(T).Name}"); + + var result = await GetEntity(snowflake, context).ConfigureAwait(false); + + return result is not null ? + TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"{option} must be a valid {typeof(T).Name} snowflake to be parsed."); + } + + public override Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult((obj as ISnowflakeEntity)?.Id.ToString()); + } + + internal sealed class DefaultUserReader : DefaultSnowflakeReader + where T : class, IUser + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } + + internal sealed class DefaultChannelReader : DefaultSnowflakeReader + where T : class, IChannel + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Client.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } + + internal sealed class DefaultRoleReader : DefaultSnowflakeReader + where T : class, IRole + { + protected override Task GetEntity(ulong id, IInteractionContext ctx) => Task.FromResult(ctx.Guild?.GetRole(id) as T); + } + + internal sealed class DefaultMessageReader : DefaultSnowflakeReader + where T : class, IMessage + { + protected override async Task GetEntity(ulong id, IInteractionContext ctx) => await ctx.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs new file mode 100644 index 000000000..e833382a6 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/DefaultValueReader.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class DefaultValueReader : TypeReader + where T : IConvertible + { + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + try + { + var converted = Convert.ChangeType(option, typeof(T)); + return Task.FromResult(TypeConverterResult.FromSuccess(converted)); + } + catch (InvalidCastException castEx) + { + return Task.FromResult(TypeConverterResult.FromError(castEx)); + } + } + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs new file mode 100644 index 000000000..df6f2ac33 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/EnumReader.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal sealed class EnumReader : TypeReader + where T : struct, Enum + { + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + { + return Task.FromResult(Enum.TryParse(option, out var result) ? + TypeConverterResult.FromSuccess(result) : TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option} cannot be converted to {nameof(T)}")); + } + + public override Task SerializeAsync(object obj, IServiceProvider services) + { + var name = Enum.GetName(typeof(T), obj); + + if (name is null) + throw new ArgumentException($"Enum name cannot be parsed from {obj}"); + + return Task.FromResult(name); + } + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs new file mode 100644 index 000000000..e518e0208 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/TypeReader.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Base class for creating TypeConverters. uses TypeConverters to interface with Slash Command parameters. + /// + public abstract class TypeReader : ITypeConverter + { + /// + /// Will be used to search for alternative TypeReaders whenever the Command Service encounters an unknown parameter type. + /// + /// An object type. + /// + /// The boolean result. + /// + public abstract bool CanConvertTo(Type type); + + /// + /// Will be used to read the incoming payload before executing the method body. + /// + /// Command execution context. + /// Received option payload. + /// Service provider that will be used to initialize the command module. + /// The result of the read process. + public abstract Task ReadAsync(IInteractionContext context, string option, IServiceProvider services); + + /// + /// Will be used to serialize objects into strings. + /// + /// Object to be serialized. + /// + /// A task representing the conversion process. The result of the task contains the conversion result. + /// + public virtual Task SerializeAsync(object obj, IServiceProvider services) => Task.FromResult(obj.ToString()); + } + + /// + public abstract class TypeReader : TypeReader + { + /// + public sealed override bool CanConvertTo(Type type) => + typeof(T).IsAssignableFrom(type); + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs index d42cc2fe9..e2d028e1f 100644 --- a/src/Discord.Net.Interactions/Utilities/ModalUtils.cs +++ b/src/Discord.Net.Interactions/Utilities/ModalUtils.cs @@ -7,20 +7,20 @@ namespace Discord.Interactions { internal static class ModalUtils { - private static ConcurrentDictionary _modalInfos = new(); + private static readonly ConcurrentDictionary _modalInfos = new(); public static IReadOnlyCollection Modals => _modalInfos.Values.ToReadOnlyCollection(); - public static ModalInfo GetOrAdd(Type type) + public static ModalInfo GetOrAdd(Type type, InteractionService interactionService) { if (!typeof(IModal).IsAssignableFrom(type)) throw new ArgumentException($"Must be an implementation of {nameof(IModal)}", nameof(type)); - return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type)); + return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService)); } - public static ModalInfo GetOrAdd() where T : class, IModal - => GetOrAdd(typeof(T)); + public static ModalInfo GetOrAdd(InteractionService interactionService) where T : class, IModal + => GetOrAdd(typeof(T), interactionService); public static bool TryGet(Type type, out ModalInfo modalInfo) { From 24b7bb593aa3b8c33321bace31680c58f3a02ea5 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:28:46 -0400 Subject: [PATCH 018/153] Fix: sharded client logout (#2179) --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 8374f2877..a361889c0 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -178,7 +178,6 @@ namespace Discord.WebSocket await _shards[i].LogoutAsync(); } - CurrentUser = null; if (_automaticShards) { _shardIds = new int[0]; From 765c0c554475efb332a6c28d935dbd9951158e73 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:28:56 -0400 Subject: [PATCH 019/153] Feature: attachment description and content type (#2180) --- .../Entities/Messages/FileAttachment.cs | 16 ++++++++++++++++ .../Entities/Messages/IAttachment.cs | 8 ++++++++ .../Entities/Messages/Attachment.cs | 12 ++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs index 35252693b..7470a72cd 100644 --- a/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/FileAttachment.cs @@ -7,13 +7,29 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents an outgoing file attachment used to send a file to discord. + /// public struct FileAttachment : IDisposable { + /// + /// Gets or sets the filename. + /// public string FileName { get; set; } + /// + /// Gets or sets the description of the file. + /// public string Description { get; set; } + + /// + /// Gets or sets whether this file should be marked as a spoiler. + /// public bool IsSpoiler { get; set; } #pragma warning disable IDISP008 + /// + /// Gets the stream containing the file content. + /// public Stream Stream { get; } #pragma warning restore IDISP008 diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index e94e9f97c..277c06291 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -62,5 +62,13 @@ namespace Discord /// if the attachment is ephemeral; otherwise . /// bool Ephemeral { get; } + /// + /// Gets the description of the attachment; or if there is none set. + /// + string Description { get; } + /// + /// Gets the media's MIME type if present; otherwise . + /// + string ContentType { get; } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index 4e4849c51..a5b83fb7b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -23,8 +23,13 @@ namespace Discord public int? Width { get; } /// public bool Ephemeral { get; } + /// + public string Description { get; } + /// + public string ContentType { get; } - internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, bool? ephemeral) + internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width, + bool? ephemeral, string description, string contentType) { Id = id; Filename = filename; @@ -34,13 +39,16 @@ namespace Discord Height = height; Width = width; Ephemeral = ephemeral.GetValueOrDefault(false); + Description = description; + ContentType = contentType; } internal static Attachment Create(Model model) { return new Attachment(model.Id, model.Filename, model.Url, model.ProxyUrl, model.Size, model.Height.IsSpecified ? model.Height.Value : (int?)null, model.Width.IsSpecified ? model.Width.Value : (int?)null, - model.Ephemeral.ToNullable()); + model.Ephemeral.ToNullable(), model.Description.GetValueOrDefault(), + model.ContentType.GetValueOrDefault()); } /// From f8ec3c79c2559288d743f5cd2fcb077cdb76306e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:29:10 -0400 Subject: [PATCH 020/153] Fix/ambigiuous reference (#2181) * fix: Ambigiuous reference when creating roles * Update RestGuild.cs --- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 5 ----- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 4 ---- 2 files changed, 9 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 2c37bb2da..e89096f00 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -763,11 +763,6 @@ namespace Discord.Rest return null; } - /// - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => CreateRoleAsync(name, permissions, color, isHoisted, false, options); - /// /// Creates a new role with the provided name. /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index bd5d811f1..c4b756410 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -999,10 +999,6 @@ namespace Discord.WebSocket return null; } - /// - public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); /// /// Creates a new role with the provided name. /// From 25aaa4948ad103556c09aeed813939d2f4ddd1a6 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:29:24 -0400 Subject: [PATCH 021/153] fix: thread owner always null (#2182) --- .../Entities/Channels/SocketThreadChannel.cs | 29 +++++++++++++++++-- .../Entities/Users/SocketThreadUser.cs | 11 +++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index c26a23afd..2e77e62e3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -24,7 +24,29 @@ namespace Discord.WebSocket /// /// Gets the owner of the current thread. /// - public SocketThreadUser Owner { get; private set; } + public SocketThreadUser Owner + { + get + { + lock (_ownerLock) + { + var user = GetUser(_ownerId); + + if (user == null) + { + var guildMember = Guild.GetUser(_ownerId); + if (guildMember == null) + return null; + + user = SocketThreadUser.Create(Guild, this, guildMember); + _members[user.Id] = user; + return user; + } + else + return user; + } + } + } /// /// Gets the current users within this thread. @@ -83,6 +105,9 @@ namespace Discord.WebSocket private bool _usersDownloaded; private readonly object _downloadLock = new object(); + private readonly object _ownerLock = new object(); + + private ulong _ownerId; internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketGuildChannel parent, DateTimeOffset? createdAt) @@ -120,7 +145,7 @@ namespace Discord.WebSocket if (model.OwnerId.IsSpecified) { - Owner = GetUser(model.OwnerId.Value); + _ownerId = model.OwnerId.Value; } HasJoined = model.ThreadMember.IsSpecified; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 025d34d0f..6eddd876d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -147,6 +147,17 @@ namespace Discord.WebSocket return entity; } + internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) + { + // this is used for creating the owner of the thread. + var entity = new SocketThreadUser(guild, thread, owner, owner.Id); + entity.Update(new Model + { + JoinTimestamp = thread.CreatedAt, + }); + return entity; + } + internal void Update(Model model) { ThreadJoinedAt = model.JoinTimestamp; From e3fc96bc44fabf1809a8a0f2b5a68e79f69c8602 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 9 Mar 2022 17:33:25 -0400 Subject: [PATCH 022/153] meta: 3.4.1 --- CHANGELOG.md | 14 +++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 47 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 416f2ec6e..a96a77e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [3.4.1] - 2022-03-9 + +### Added +- #2169 Component TypeConverters and CustomID TypeReaders (fb4250b) +- #2180 Attachment description and content type (765c0c5) +- #2162 Add configuration toggle to suppress Unknown dispatch warnings (1ba96d6) +- #2178 Add 10065 Error code (cc6918d) + +### Fixed +- #2179 Logging out sharded client throws (24b7bb5) +- #2182 Thread owner always returns null (25aaa49) +- #2165 Fix error with flag params when uploading files. (a5d3add) +- #2181 Fix ambiguous reference for creating roles (f8ec3c7) + ## [3.4.0] - 2022-3-2 ## Added diff --git a/Discord.Net.targets b/Discord.Net.targets index d0e17b3c5..187ff9d75 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.4.0 + 3.4.1 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 2ad0164f4..3b7ef582b 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.4.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.4.1", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index d98287ffa..996e9bae9 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.4.0$suffix$ + 3.4.1$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From fc31589056601a16a61f4404f67721ee05330187 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 9 Mar 2022 17:36:29 -0400 Subject: [PATCH 023/153] Fix changelog formatting --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a96a77e17..6884d3564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ ## [3.4.0] - 2022-3-2 -## Added +### Added - #2146 Add FromDateTimeOffset in TimestampTag (553055b) - #2062 Add return statement to precondition handling (3e52fab) - #2131 Add support for sending Message Flags (1fb62de) @@ -27,13 +27,13 @@ - #2155 Add Interaction Service Complex Parameters (9ba64f6) - #2156 Add Display name support for enum type converter (c800674) -## Fixed +### Fixed - #2117 Fix stream access exception when ratelimited (a1cfa41) - #2128 Fix context menu comand message type (f601e9b) - #2135 Fix NRE when ratelimmited requests don't return a body (b95b942) - #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3) -## Misc +### Misc - #2149 Clarify Users property on SocketGuildChannel (5594739) - #2157 Enforce valid button styles (507a18d) From c286b9978ed13056651a7202a506d4fdeea218d3 Mon Sep 17 00:00:00 2001 From: Raiden Shogun <96011415+Almighty-Shogun@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:33:21 +0100 Subject: [PATCH 024/153] Fixed typo (#2206) `await arg2.Interaction.RespondAsync("Command exception: {arg3.ErrorReason}");` would never have showed the `ErrorReason` because a `$` was missing before the string. --- docs/guides/int_framework/samples/postexecution/error_review.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_framework/samples/postexecution/error_review.cs b/docs/guides/int_framework/samples/postexecution/error_review.cs index dd397b2c9..d5f8a9cb1 100644 --- a/docs/guides/int_framework/samples/postexecution/error_review.cs +++ b/docs/guides/int_framework/samples/postexecution/error_review.cs @@ -16,7 +16,7 @@ async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionConte await arg2.Interaction.RespondAsync("Invalid number or arguments"); break; case InteractionCommandError.Exception: - await arg2.Interaction.RespondAsync("Command exception:{arg3.ErrorReason}"); + await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}"); break; case InteractionCommandError.Unsuccessful: await arg2.Interaction.RespondAsync("Command could not be executed"); From 47de5a2fb450a846a805f5185e8bf7bf46c533ca Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:37:30 +0100 Subject: [PATCH 025/153] Greatly reduce code complexity & make IF samples functional (#2205) * Greatly reduce code complexity * Fixes sharded client IF implementation --- .../InteractionFramework/CommandHandler.cs | 152 ------------------ .../{ => Enums}/ExampleEnum.cs | 0 .../InteractionHandler.cs | 81 ++++++++++ .../Modules/ComponentModule.cs | 18 --- .../{GeneralModule.cs => ExampleModule.cs} | 84 +++++----- .../Modules/MessageCommandModule.cs | 30 ---- .../Modules/SlashCommandModule.cs | 51 ------ .../Modules/UserCommandModule.cs | 17 -- samples/InteractionFramework/Program.cs | 75 ++++----- .../Modules/InteractionModule.cs | 2 +- samples/ShardedClient/Program.cs | 7 +- .../Services/InteractionHandlingService.cs | 4 +- 12 files changed, 170 insertions(+), 351 deletions(-) delete mode 100644 samples/InteractionFramework/CommandHandler.cs rename samples/InteractionFramework/{ => Enums}/ExampleEnum.cs (100%) create mode 100644 samples/InteractionFramework/InteractionHandler.cs delete mode 100644 samples/InteractionFramework/Modules/ComponentModule.cs rename samples/InteractionFramework/Modules/{GeneralModule.cs => ExampleModule.cs} (54%) delete mode 100644 samples/InteractionFramework/Modules/MessageCommandModule.cs delete mode 100644 samples/InteractionFramework/Modules/SlashCommandModule.cs delete mode 100644 samples/InteractionFramework/Modules/UserCommandModule.cs diff --git a/samples/InteractionFramework/CommandHandler.cs b/samples/InteractionFramework/CommandHandler.cs deleted file mode 100644 index 9a505246f..000000000 --- a/samples/InteractionFramework/CommandHandler.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - public class CommandHandler - { - private readonly DiscordSocketClient _client; - private readonly InteractionService _commands; - private readonly IServiceProvider _services; - - public CommandHandler(DiscordSocketClient client, InteractionService commands, IServiceProvider services) - { - _client = client; - _commands = commands; - _services = services; - } - - public async Task InitializeAsync ( ) - { - // Add the public modules that inherit InteractionModuleBase to the InteractionService - await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); - // Another approach to get the assembly of a specific type is: - // typeof(CommandHandler).Assembly - - - // Process the InteractionCreated payloads to execute Interactions commands - _client.InteractionCreated += HandleInteraction; - - // Process the command execution results - _commands.SlashCommandExecuted += SlashCommandExecuted; - _commands.ContextCommandExecuted += ContextCommandExecuted; - _commands.ComponentCommandExecuted += ComponentCommandExecuted; - } - - # region Error Handling - - private Task ComponentCommandExecuted (ComponentCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - - private Task ContextCommandExecuted (ContextCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - - private Task SlashCommandExecuted (SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) - { - if (!arg3.IsSuccess) - { - switch (arg3.Error) - { - case InteractionCommandError.UnmetPrecondition: - // implement - break; - case InteractionCommandError.UnknownCommand: - // implement - break; - case InteractionCommandError.BadArgs: - // implement - break; - case InteractionCommandError.Exception: - // implement - break; - case InteractionCommandError.Unsuccessful: - // implement - break; - default: - break; - } - } - - return Task.CompletedTask; - } - # endregion - - # region Execution - - private async Task HandleInteraction (SocketInteraction arg) - { - try - { - // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules - var ctx = new SocketInteractionContext(_client, arg); - await _commands.ExecuteCommandAsync(ctx, _services); - } - catch (Exception ex) - { - Console.WriteLine(ex); - - // If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original - // response, or at least let the user know that something went wrong during the command execution. - if(arg.Type == InteractionType.ApplicationCommand) - await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); - } - } - # endregion - } -} diff --git a/samples/InteractionFramework/ExampleEnum.cs b/samples/InteractionFramework/Enums/ExampleEnum.cs similarity index 100% rename from samples/InteractionFramework/ExampleEnum.cs rename to samples/InteractionFramework/Enums/ExampleEnum.cs diff --git a/samples/InteractionFramework/InteractionHandler.cs b/samples/InteractionFramework/InteractionHandler.cs new file mode 100644 index 000000000..bc6f47285 --- /dev/null +++ b/samples/InteractionFramework/InteractionHandler.cs @@ -0,0 +1,81 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using Microsoft.Extensions.Configuration; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace InteractionFramework +{ + public class InteractionHandler + { + private readonly DiscordSocketClient _client; + private readonly InteractionService _handler; + private readonly IServiceProvider _services; + private readonly IConfiguration _configuration; + + public InteractionHandler(DiscordSocketClient client, InteractionService handler, IServiceProvider services, IConfiguration config) + { + _client = client; + _handler = handler; + _services = services; + _configuration = config; + } + + public async Task InitializeAsync() + { + // Process when the client is ready, so we can register our commands. + _client.Ready += ReadyAsync; + _handler.Log += LogAsync; + + // Add the public modules that inherit InteractionModuleBase to the InteractionService + await _handler.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + + // Process the InteractionCreated payloads to execute Interactions commands + _client.InteractionCreated += HandleInteraction; + } + + private async Task LogAsync(LogMessage log) + => Console.WriteLine(log); + + private async Task ReadyAsync() + { + // Context & Slash commands can be automatically registered, but this process needs to happen after the client enters the READY state. + // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. + if (Program.IsDebug()) + await _handler.RegisterCommandsToGuildAsync(_configuration.GetValue("testGuild"), true); + else + await _handler.RegisterCommandsGloballyAsync(true); + } + + private async Task HandleInteraction(SocketInteraction interaction) + { + try + { + // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules. + var context = new SocketInteractionContext(_client, interaction); + + // Execute the incoming command. + var result = await _handler.ExecuteCommandAsync(context, _services); + + if (!result.IsSuccess) + switch (result.Error) + { + case InteractionCommandError.UnmetPrecondition: + // implement + break; + default: + break; + } + } + catch + { + // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original + // response, or at least let the user know that something went wrong during the command execution. + if (interaction.Type is InteractionType.ApplicationCommand) + await interaction.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); + } + } + } +} diff --git a/samples/InteractionFramework/Modules/ComponentModule.cs b/samples/InteractionFramework/Modules/ComponentModule.cs deleted file mode 100644 index 643004ded..000000000 --- a/samples/InteractionFramework/Modules/ComponentModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Discord.Interactions; -using Discord.WebSocket; -using InteractionFramework.Attributes; -using System.Threading.Tasks; - -namespace InteractionFramework -{ - // As with all other modules, we create the context by defining what type of interaction this module is supposed to target. - internal class ComponentModule : InteractionModuleBase> - { - // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. - // See Attributes/DoUserCheckAttribute.cs for elaboration. - [DoUserCheck] - [ComponentInteraction("myButton:*")] - public async Task ClickButtonAsync(string userId) - => await RespondAsync(text: ":thumbsup: Clicked!"); - } -} diff --git a/samples/InteractionFramework/Modules/GeneralModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs similarity index 54% rename from samples/InteractionFramework/Modules/GeneralModule.cs rename to samples/InteractionFramework/Modules/ExampleModule.cs index 78740a960..1c0a6c8a2 100644 --- a/samples/InteractionFramework/Modules/GeneralModule.cs +++ b/samples/InteractionFramework/Modules/ExampleModule.cs @@ -1,32 +1,25 @@ using Discord; using Discord.Interactions; +using InteractionFramework.Attributes; +using System; using System.Threading.Tasks; namespace InteractionFramework.Modules { // Interation modules must be public and inherit from an IInterationModuleBase - public class GeneralModule : InteractionModuleBase + public class ExampleModule : InteractionModuleBase { // Dependencies can be accessed through Property injection, public properties with public setters will be set by the service provider public InteractionService Commands { get; set; } - private CommandHandler _handler; + private InteractionHandler _handler; // Constructor injection is also a valid way to access the dependecies - public GeneralModule(CommandHandler handler) + public ExampleModule(InteractionHandler handler) { _handler = handler; } - // Slash Commands are declared using the [SlashCommand], you need to provide a name and a description, both following the Discord guidelines - [SlashCommand("ping", "Recieve a pong")] - // By setting the DefaultPermission to false, you can disable the command by default. No one can use the command until you give them permission - [DefaultPermission(false)] - public async Task Ping ( ) - { - await RespondAsync("pong"); - } - // You can use a number of parameter types in you Slash Command handlers (string, int, double, bool, IUser, IChannel, IMentionable, IRole, Enums) by default. Optionally, // you can implement your own TypeConverters to support a wider range of parameter types. For more information, refer to the library documentation. // Optional method parameters(parameters with a default value) also will be displayed as optional on Discord. @@ -34,9 +27,15 @@ namespace InteractionFramework.Modules // [Summary] lets you customize the name and the description of a parameter [SlashCommand("echo", "Repeat the input")] public async Task Echo(string echo, [Summary(description: "mention the user")]bool mention = false) - { - await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); - } + => await RespondAsync(echo + (mention ? Context.User.Mention : string.Empty)); + + [SlashCommand("ping", "Pings the bot and returns its latency.")] + public async Task GreetUserAsync() + => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); + + [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] + public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) + => await RespondAsync(text: $"This voice channel has a bitrate of {(channel as IVoiceChannel).Bitrate}"); // [Group] will create a command group. [SlashCommand]s and [ComponentInteraction]s will be registered with the group prefix [Group("test_group", "This is a command group")] @@ -46,25 +45,7 @@ namespace InteractionFramework.Modules // choice option [SlashCommand("choice_example", "Enums create choices")] public async Task ChoiceExample(ExampleEnum input) - { - await RespondAsync(input.ToString()); - } - } - - // User Commands can only have one parameter, which must be a type of SocketUser - [UserCommand("SayHello")] - public async Task SayHello(IUser user) - { - await RespondAsync($"Hello, {user.Mention}"); - } - - // Message Commands can only have one parameter, which must be a type of SocketMessage - [MessageCommand("Delete")] - [Attributes.RequireOwner] - public async Task DeleteMesage(IMessage message) - { - await message.DeleteAsync(); - await RespondAsync("Deleted message."); + => await RespondAsync(input.ToString()); } // Use [ComponentInteraction] to handle message component interactions. Message component interaction with the matching customId will be executed. @@ -80,9 +61,40 @@ namespace InteractionFramework.Modules // Select Menu interactions, contain ids of the menu options that were selected by the user. You can access the option ids from the method parameters. // You can also use the wild card pattern with Select Menus, in that case, the wild card captures will be passed on to the method first, followed by the option ids. [ComponentInteraction("roleSelect")] - public async Task RoleSelect(params string[] selections) + public async Task RoleSelect(string[] selections) + { + throw new NotImplementedException(); + } + + // With the Attribute DoUserCheck you can make sure that only the user this button targets can click it. This is defined by the first wildcard: *. + // See Attributes/DoUserCheckAttribute.cs for elaboration. + [DoUserCheck] + [ComponentInteraction("myButton:*")] + public async Task ClickButtonAsync(string userId) + => await RespondAsync(text: ":thumbsup: Clicked!"); + + // This command will greet target user in the channel this was executed in. + [UserCommand("greet")] + public async Task GreetUserAsync(IUser user) + => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); + + // Pins a message in the channel it is in. + [MessageCommand("pin")] + public async Task PinMessageAsync(IMessage message) { - // implement + // make a safety cast to check if the message is ISystem- or IUserMessage + if (message is not IUserMessage userMessage) + await RespondAsync(text: ":x: You cant pin system messages!"); + + // if the pins in this channel are equal to or above 50, no more messages can be pinned. + else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) + await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); + + else + { + await userMessage.PinAsync(); + await RespondAsync(":white_check_mark: Successfully pinned message!"); + } } } } diff --git a/samples/InteractionFramework/Modules/MessageCommandModule.cs b/samples/InteractionFramework/Modules/MessageCommandModule.cs deleted file mode 100644 index d07d276f5..000000000 --- a/samples/InteractionFramework/Modules/MessageCommandModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - internal class MessageCommandModule : InteractionModuleBase> - { - // Pins a message in the channel it is in. - [MessageCommand("pin")] - public async Task PinMessageAsync(IMessage message) - { - // make a safety cast to check if the message is ISystem- or IUserMessage - if (message is not IUserMessage userMessage) - await RespondAsync(text: ":x: You cant pin system messages!"); - - // if the pins in this channel are equal to or above 50, no more messages can be pinned. - else if ((await Context.Channel.GetPinnedMessagesAsync()).Count >= 50) - await RespondAsync(text: ":x: You cant pin any more messages, the max has already been reached in this channel!"); - - else - { - await userMessage.PinAsync(); - await RespondAsync(":white_check_mark: Successfully pinned message!"); - } - } - } -} diff --git a/samples/InteractionFramework/Modules/SlashCommandModule.cs b/samples/InteractionFramework/Modules/SlashCommandModule.cs deleted file mode 100644 index a066ea18c..000000000 --- a/samples/InteractionFramework/Modules/SlashCommandModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - public enum Hobby - { - Gaming, - - Art, - - Reading - } - - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class SlashCommandModule : InteractionModuleBase> - { - // Will be called before execution. Here you can populate several entities you may want to retrieve before executing a command. - // I.E. database objects - public override void BeforeExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - // Will be called after execution - public override void AfterExecute(ICommandInfo command) - { - // Anything - throw new NotImplementedException(); - } - - [SlashCommand("ping", "Pings the bot and returns its latency.")] - public async Task GreetUserAsync() - => await RespondAsync(text: $":ping_pong: It took me {Context.Client.Latency}ms to respond to you!", ephemeral: true); - - [SlashCommand("hobby", "Choose your hobby from the list!")] - public async Task ChooseAsync(Hobby hobby) - => await RespondAsync(text: $":thumbsup: Your hobby is: {hobby}."); - - [SlashCommand("bitrate", "Gets the bitrate of a specific voice channel.")] - public async Task GetBitrateAsync([ChannelTypes(ChannelType.Voice, ChannelType.Stage)] IChannel channel) - { - var voiceChannel = channel as IVoiceChannel; - await RespondAsync(text: $"This voice channel has a bitrate of {voiceChannel.Bitrate}"); - } - } -} diff --git a/samples/InteractionFramework/Modules/UserCommandModule.cs b/samples/InteractionFramework/Modules/UserCommandModule.cs deleted file mode 100644 index 60c5246ce..000000000 --- a/samples/InteractionFramework/Modules/UserCommandModule.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord; -using Discord.Interactions; -using Discord.WebSocket; -using System.Threading.Tasks; - -namespace InteractionFramework.Modules -{ - // A transient module for executing commands. This module will NOT keep any information after the command is executed. - class UserCommandModule : InteractionModuleBase> - { - // This command will greet target user in the channel this was executed in. - [UserCommand("greet")] - public async Task GreetUserAsync(IUser user) - => await RespondAsync(text: $":wave: {Context.User} said hi to you, <@{user.Id}>!"); - } -} - diff --git a/samples/InteractionFramework/Program.cs b/samples/InteractionFramework/Program.cs index 49db29714..b9c4697af 100644 --- a/samples/InteractionFramework/Program.cs +++ b/samples/InteractionFramework/Program.cs @@ -9,69 +9,60 @@ using System.Threading.Tasks; namespace InteractionFramework { - class Program + public class Program { - // Entry point of the program. - static void Main ( string[] args ) + private readonly IConfiguration _configuration; + private readonly IServiceProvider _services; + + private readonly DiscordSocketConfig _socketConfig = new() + { + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers, + AlwaysDownloadUsers = true, + }; + + public Program() { - // One of the more flexable ways to access the configuration data is to use the Microsoft's Configuration model, - // this way we can avoid hard coding the environment secrets. I opted to use the Json and environment variable providers here. - IConfiguration config = new ConfigurationBuilder() + _configuration = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "DC_") .AddJsonFile("appsettings.json", optional: true) .Build(); - RunAsync(config).GetAwaiter().GetResult(); + _services = new ServiceCollection() + .AddSingleton(_configuration) + .AddSingleton(_socketConfig) + .AddSingleton() + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .AddSingleton() + .BuildServiceProvider(); } - static async Task RunAsync (IConfiguration configuration) - { - // Dependency injection is a key part of the Interactions framework but it needs to be disposed at the end of the app's lifetime. - using var services = ConfigureServices(configuration); + static void Main(string[] args) + => new Program().RunAsync() + .GetAwaiter() + .GetResult(); - var client = services.GetRequiredService(); - var commands = services.GetRequiredService(); + public async Task RunAsync() + { + var client = _services.GetRequiredService(); client.Log += LogAsync; - commands.Log += LogAsync; - - // Slash Commands and Context Commands are can be automatically registered, but this process needs to happen after the client enters the READY state. - // Since Global Commands take around 1 hour to register, we should use a test guild to instantly update and test our commands. To determine the method we should - // register the commands with, we can check whether we are in a DEBUG environment and if we are, we can register the commands to a predetermined test guild. - client.Ready += async ( ) => - { - if (IsDebug()) - // Id of the test guild can be provided from the Configuration object - await commands.RegisterCommandsToGuildAsync(configuration.GetValue("testGuild"), true); - else - await commands.RegisterCommandsGloballyAsync(true); - }; // Here we can initialize the service that will register and execute our commands - await services.GetRequiredService().InitializeAsync(); + await _services.GetRequiredService() + .InitializeAsync(); // Bot token can be provided from the Configuration object we set up earlier - await client.LoginAsync(TokenType.Bot, configuration["token"]); + await client.LoginAsync(TokenType.Bot, _configuration["token"]); await client.StartAsync(); + // Never quit the program until manually forced to. await Task.Delay(Timeout.Infinite); } - static Task LogAsync(LogMessage message) - { - Console.WriteLine(message.ToString()); - return Task.CompletedTask; - } - - static ServiceProvider ConfigureServices ( IConfiguration configuration ) - => new ServiceCollection() - .AddSingleton(configuration) - .AddSingleton() - .AddSingleton(x => new InteractionService(x.GetRequiredService())) - .AddSingleton() - .BuildServiceProvider(); + private async Task LogAsync(LogMessage message) + => Console.WriteLine(message.ToString()); - static bool IsDebug ( ) + public static bool IsDebug() { #if DEBUG return true; diff --git a/samples/ShardedClient/Modules/InteractionModule.cs b/samples/ShardedClient/Modules/InteractionModule.cs index 089328e7d..6c2f0e940 100644 --- a/samples/ShardedClient/Modules/InteractionModule.cs +++ b/samples/ShardedClient/Modules/InteractionModule.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace ShardedClient.Modules { // A display of portability, which shows how minimal the difference between the 2 frameworks is. - public class InteractionModule : InteractionModuleBase> + public class InteractionModule : InteractionModuleBase { [SlashCommand("info", "Information about this shard.")] public async Task InfoAsync() diff --git a/samples/ShardedClient/Program.cs b/samples/ShardedClient/Program.cs index 717ce1d80..2b8f49edb 100644 --- a/samples/ShardedClient/Program.cs +++ b/samples/ShardedClient/Program.cs @@ -45,8 +45,11 @@ namespace ShardedClient client.ShardReady += ReadyAsync; client.Log += LogAsync; - await services.GetRequiredService().InitializeAsync(); - await services.GetRequiredService().InitializeAsync(); + await services.GetRequiredService() + .InitializeAsync(); + + await services.GetRequiredService() + .InitializeAsync(); // Tokens should be considered secret data, and never hard-coded. await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); diff --git a/samples/ShardedClient/Services/InteractionHandlingService.cs b/samples/ShardedClient/Services/InteractionHandlingService.cs index 59b479361..3c41d7f33 100644 --- a/samples/ShardedClient/Services/InteractionHandlingService.cs +++ b/samples/ShardedClient/Services/InteractionHandlingService.cs @@ -31,9 +31,9 @@ namespace ShardedClient.Services { await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider); #if DEBUG - await _service.AddCommandsToGuildAsync(_client.Guilds.First(x => x.Id == 1)); + await _service.RegisterCommandsToGuildAsync(1 /* implement */); #else - await _service.AddCommandsGloballyAsync(); + await _service.RegisterCommandsGloballyAsync(); #endif } From d5342e458500d42b94f7b1471f96c40ebfc56b35 Mon Sep 17 00:00:00 2001 From: Robin Sue Date: Sat, 26 Mar 2022 13:42:07 +0100 Subject: [PATCH 026/153] Fix Serilog Level Mapping (#2202) --- docs/guides/other_libs/samples/ModifyLogMethod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs index 0f7c11daf..b4870cfd1 100644 --- a/docs/guides/other_libs/samples/ModifyLogMethod.cs +++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs @@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message) LogSeverity.Error => LogEventLevel.Error, LogSeverity.Warning => LogEventLevel.Warning, LogSeverity.Info => LogEventLevel.Information, - LogSeverity.Verbose => LogEventLevel.Verbose, - LogSeverity.Debug => LogEventLevel.Debug, + LogSeverity.Verbose => LogEventLevel.Debug, + LogSeverity.Debug => LogEventLevel.Verbose, _ => LogEventLevel.Information }; Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); From 82473bce69f323448dff8cda3c1ddda4bbf5a383 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:43:16 +0100 Subject: [PATCH 027/153] Update GuildMemberUpdated comment regarding presence (#2193) --- src/Discord.Net.WebSocket/BaseSocketClient.Events.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 134f8136b..b8d3b6a10 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -451,7 +451,7 @@ namespace Discord.WebSocket remove { _userUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); - /// Fired when a guild member is updated, or a member presence is updated. + /// Fired when a guild member is updated. public event Func, SocketGuildUser, Task> GuildMemberUpdated { add { _guildMemberUpdatedEvent.Add(value); } From 741ed809d64dd6c9a364f5ab6d8f82b5000677b5 Mon Sep 17 00:00:00 2001 From: d4n Date: Sat, 26 Mar 2022 07:44:13 -0500 Subject: [PATCH 028/153] Add missing methods to IComponentInteraction (#2201) --- .../IComponentInteraction.cs | 21 ++++++++++++++++++- .../MessageComponents/RestMessageComponent.cs | 8 +++++++ .../SocketMessageComponent.cs | 16 ++------------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs index 2a46e8f18..299ee795d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteraction.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading.Tasks; + namespace Discord { /// @@ -6,7 +9,7 @@ namespace Discord public interface IComponentInteraction : IDiscordInteraction { /// - /// Gets the data received with this interaction, contains the button that was clicked. + /// Gets the data received with this component interaction. /// new IComponentInteractionData Data { get; } @@ -14,5 +17,21 @@ namespace Discord /// Gets the message that contained the trigger for this interaction. /// IUserMessage Message { get; } + + /// + /// Updates the message which this component resides in with the type + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// A task that represents the asynchronous operation of updating the message. + Task UpdateAsync(Action func, RequestOptions options = null); + + /// + /// Defers an interaction with the response type 5 (). + /// + /// to defer ephemerally, otherwise . + /// The options to be used when sending the request. + /// A task that represents the asynchronous operation of acknowledging the interaction. + Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 359b92249..002510eac 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -492,5 +492,13 @@ namespace Discord.Rest /// IUserMessage IComponentInteraction.Message => Message; + + /// + Task IComponentInteraction.UpdateAsync(Action func, RequestOptions options) + => Task.FromResult(Update(func, options)); + + /// + Task IComponentInteraction.DeferLoadingAsync(bool ephemeral, RequestOptions options) + => Task.FromResult(DeferLoading(ephemeral, options)); } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index b06979381..aeff465bd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -202,12 +202,7 @@ namespace Discord.WebSocket HasResponded = true; } - /// - /// Updates the message which this component resides in with the type - /// - /// A delegate containing the properties to modify the message with. - /// The request options for this request. - /// A task that represents the asynchronous operation of updating the message. + /// public async Task UpdateAsync(Action func, RequestOptions options = null) { var args = new MessageProperties(); @@ -383,14 +378,7 @@ namespace Discord.WebSocket return await InteractionHelper.SendFollowupAsync(Discord, args, Token, Channel, options).ConfigureAwait(false); } - /// - /// Defers an interaction and responds with type 5 () - /// - /// to send this message ephemerally, otherwise . - /// The request options for this request. - /// - /// A task that represents the asynchronous operation of acknowledging the interaction. - /// + /// public async Task DeferLoadingAsync(bool ephemeral = false, RequestOptions options = null) { if (!InteractionHelper.CanSendResponse(this)) From d48a7bd3483bb9ba414feb70740633877e77334c Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 26 Mar 2022 13:45:54 +0100 Subject: [PATCH 029/153] Fix: serialization error on thread creation timestamp. (#2188) --- src/Discord.Net.Rest/API/Common/ThreadMetadata.cs | 2 +- .../Entities/Channels/SocketThreadChannel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs index 15854fab4..6735504c8 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -21,6 +21,6 @@ namespace Discord.API public Optional Invitable { get; set; } [JsonProperty("create_timestamp")] - public Optional CreatedAt { get; set; } + public Optional CreatedAt { get; set; } } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 2e77e62e3..78462b062 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -121,7 +121,7 @@ namespace Discord.WebSocket internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) { var parent = guild.GetChannel(model.CategoryId.Value); - var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.ToNullable()); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault(null)); entity.Update(state, model); return entity; } From d656722bd9477c410832e2d8c22b6baf1fb2f960 Mon Sep 17 00:00:00 2001 From: KeylAmi Date: Sat, 26 Mar 2022 08:46:33 -0400 Subject: [PATCH 030/153] Fix: modal response failing (#2187) * Update bugreport.yml * Update bugreport.yml removed d.net reference. fixed spelling. * Update bugreport.yml Adjusted verbiage for clarity * Fix for modal response failing Credit to @Cenggo for finding issue. --- src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs index a55a1307a..4866bd1da 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -49,7 +49,7 @@ namespace Discord.Interactions try { var args = new object[Parameters.Count]; - var captureCount = additionalArgs.Length; + var captureCount = additionalArgs?.Length ?? 0; for(var i = 0; i < Parameters.Count; i++) { From 305d7f9e137b86e50412204e7dd4aa2dfe733094 Mon Sep 17 00:00:00 2001 From: FeroxFoxxo Date: Sun, 27 Mar 2022 01:52:31 +1300 Subject: [PATCH 031/153] Fix: Integration model from GuildIntegration and added INTEGRATION gateway events (#2168) * fix integration models; add integration events * fix description on IGUILD for integration * fix typo in integration documentation * fix documentation in connection visibility * removed public identitiers from app and connection * Removed REST endpoints that are not part of the API. * Added documentation for rest integrations * added optional types * Fixed rest interaction field with not being IsSpecified --- .../Guilds/GuildIntegrationProperties.cs | 21 ---- .../Entities/Guilds/IGuild.cs | 21 +++- .../Entities/Guilds/IntegrationAccount.cs | 18 --- .../IIntegration.cs} | 47 ++++++-- .../Integrations/IIntegrationAccount.cs | 23 ++++ .../Integrations/IIntegrationApplication.cs | 33 ++++++ .../Integrations/IntegrationExpireBehavior.cs | 17 +++ .../Entities/Users/ConnectionVisibility.cs | 17 +++ .../Entities/Users/IConnection.cs | 59 +++++++--- src/Discord.Net.Rest/API/Common/Connection.cs | 18 ++- .../API/Common/Integration.cs | 25 +++-- .../API/Common/IntegrationAccount.cs | 2 +- .../API/Common/IntegrationApplication.cs | 20 ++++ src/Discord.Net.Rest/ClientHelper.cs | 2 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 39 +------ .../Entities/Guilds/GuildHelper.cs | 16 +-- .../Entities/Guilds/RestGuild.cs | 12 +- .../Entities/Guilds/RestGuildIntegration.cs | 104 ------------------ .../Entities/Integrations/RestIntegration.cs | 102 +++++++++++++++++ .../Integrations/RestIntegrationAccount.cs | 29 +++++ .../RestIntegrationApplication.cs | 39 +++++++ .../Entities/Users/RestConnection.cs | 53 ++++++--- .../API/Gateway/IntegrationDeletedEvent.cs | 14 +++ .../BaseSocketClient.Events.cs | 26 +++++ .../DiscordSocketClient.cs | 86 +++++++++++++++ .../Entities/Guilds/SocketGuild.cs | 12 +- 26 files changed, 596 insertions(+), 259 deletions(-) delete mode 100644 src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs delete mode 100644 src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs rename src/Discord.Net.Core/Entities/{Guilds/IGuildIntegration.cs => Integrations/IIntegration.cs} (61%) create mode 100644 src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs create mode 100644 src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs create mode 100644 src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs create mode 100644 src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs create mode 100644 src/Discord.Net.Rest/API/Common/IntegrationApplication.cs delete mode 100644 src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs create mode 100644 src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs create mode 100644 src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs create mode 100644 src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs deleted file mode 100644 index 2ca19b50a..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Discord -{ - /// - /// Provides properties used to modify an with the specified changes. - /// - public class GuildIntegrationProperties - { - /// - /// Gets or sets the behavior when an integration subscription lapses. - /// - public Optional ExpireBehavior { get; set; } - /// - /// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions. - /// - public Optional ExpireGracePeriod { get; set; } - /// - /// Gets or sets whether emoticons should be synced for this integration. - /// - public Optional EnableEmoticons { get; set; } - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 3111ff495..b4625abbf 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -718,8 +718,25 @@ namespace Discord /// Task> GetVoiceRegionsAsync(RequestOptions options = null); - Task> GetIntegrationsAsync(RequestOptions options = null); - Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); + /// + /// Gets a collection of all the integrations this guild contains. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of + /// integrations the guild can has. + /// + Task> GetIntegrationsAsync(RequestOptions options = null); + + /// + /// Deletes an integration. + /// + /// The id for the integration. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task DeleteIntegrationAsync(ulong id, RequestOptions options = null); /// /// Gets a collection of all invites in this guild. diff --git a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs b/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs deleted file mode 100644 index 340115fde..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/IntegrationAccount.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public struct IntegrationAccount - { - /// Gets the ID of the account. - /// A unique identifier of this integration account. - public string Id { get; } - /// Gets the name of the account. - /// A string containing the name of this integration account. - public string Name { get; private set; } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id})"; - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegration.cs similarity index 61% rename from src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs rename to src/Discord.Net.Core/Entities/Integrations/IIntegration.cs index 6fe3f7b55..304d58792 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildIntegration.cs +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegration.cs @@ -3,15 +3,16 @@ using System; namespace Discord { /// - /// Holds information for a guild integration feature. + /// Holds information for an integration feature. + /// Nullable fields not provided for Discord bot integrations, but are for Twitch etc. /// - public interface IGuildIntegration + public interface IIntegration { /// /// Gets the integration ID. /// /// - /// An representing the unique identifier value of this integration. + /// A representing the unique identifier value of this integration. /// ulong Id { get; } /// @@ -45,30 +46,52 @@ namespace Discord /// /// true if this integration is syncing; otherwise false. /// - bool IsSyncing { get; } + bool? IsSyncing { get; } /// /// Gets the ID that this integration uses for "subscribers". /// - ulong ExpireBehavior { get; } + ulong? RoleId { get; } + /// + /// Gets whether emoticons should be synced for this integration (twitch only currently). + /// + bool? HasEnabledEmoticons { get; } + /// + /// Gets the behavior of expiring subscribers. + /// + IntegrationExpireBehavior? ExpireBehavior { get; } /// /// Gets the grace period before expiring "subscribers". /// - ulong ExpireGracePeriod { get; } + int? ExpireGracePeriod { get; } + /// + /// Gets the user for this integration. + /// + IUser User { get; } + /// + /// Gets integration account information. + /// + IIntegrationAccount Account { get; } /// /// Gets when this integration was last synced. /// /// /// A containing a date and time of day when the integration was last synced. /// - DateTimeOffset SyncedAt { get; } + DateTimeOffset? SyncedAt { get; } /// - /// Gets integration account information. + /// Gets how many subscribers this integration has. /// - IntegrationAccount Account { get; } - + int? SubscriberCount { get; } + /// + /// Gets whether this integration been revoked. + /// + bool? IsRevoked { get; } + /// + /// Gets the bot/OAuth2 application for a discord integration. + /// + IIntegrationApplication Application { get; } + IGuild Guild { get; } ulong GuildId { get; } - ulong RoleId { get; } - IUser User { get; } } } diff --git a/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs new file mode 100644 index 000000000..322ffa5c2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegrationAccount.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// Provides the account information for an . + /// + public interface IIntegrationAccount + { + /// + /// Gets the ID of the account. + /// + /// + /// A unique identifier of this integration account. + /// + string Id { get; } + /// + /// Gets the name of the account. + /// + /// + /// A string containing the name of this integration account. + /// + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs b/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs new file mode 100644 index 000000000..9085ae686 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IIntegrationApplication.cs @@ -0,0 +1,33 @@ +namespace Discord +{ + /// + /// Provides the bot/OAuth2 application for an . + /// + public interface IIntegrationApplication + { + /// + /// Gets the id of the app. + /// + ulong Id { get; } + /// + /// Gets the name of the app. + /// + string Name { get; } + /// + /// Gets the icon hash of the app. + /// + string Icon { get; } + /// + /// Gets the description of the app. + /// + string Description { get; } + /// + /// Gets the summary of the app. + /// + string Summary { get; } + /// + /// Gets the bot associated with this application. + /// + IUser Bot { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs b/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs new file mode 100644 index 000000000..642e247eb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Integrations/IntegrationExpireBehavior.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// The behavior of expiring subscribers for an . + /// + public enum IntegrationExpireBehavior + { + /// + /// Removes a role from an expired subscriber. + /// + RemoveRole = 0, + /// + /// Kicks an expired subscriber from the guild. + /// + Kick = 1 + } +} diff --git a/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs b/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs new file mode 100644 index 000000000..ed041c9f9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/ConnectionVisibility.cs @@ -0,0 +1,17 @@ +namespace Discord +{ + /// + /// The visibility of the connected account. + /// + public enum ConnectionVisibility + { + /// + /// Invisible to everyone except the user themselves. + /// + None = 0, + /// + /// Visible to everyone. + /// + Everyone = 1 + } +} diff --git a/src/Discord.Net.Core/Entities/Users/IConnection.cs b/src/Discord.Net.Core/Entities/Users/IConnection.cs index 1e65d971f..94b23a4b5 100644 --- a/src/Discord.Net.Core/Entities/Users/IConnection.cs +++ b/src/Discord.Net.Core/Entities/Users/IConnection.cs @@ -4,24 +4,53 @@ namespace Discord { public interface IConnection { - /// Gets the ID of the connection account. - /// A representing the unique identifier value of this connection. + /// + /// Gets the ID of the connection account. + /// + /// + /// A representing the unique identifier value of this connection. + /// string Id { get; } - /// Gets the service of the connection (twitch, youtube). - /// A string containing the name of this type of connection. - string Type { get; } - /// Gets the username of the connection account. - /// A string containing the name of this connection. + /// + /// Gets the username of the connection account. + /// + /// + /// A string containing the name of this connection. + /// string Name { get; } - /// Gets whether the connection is revoked. - /// A value which if true indicates that this connection has been revoked, otherwise false. - bool IsRevoked { get; } - - /// Gets a of integration IDs. + /// + /// Gets the service of the connection (twitch, youtube). + /// + /// + /// A string containing the name of this type of connection. + /// + string Type { get; } + /// + /// Gets whether the connection is revoked. + /// /// - /// An containing - /// representations of unique identifier values of integrations. + /// A value which if true indicates that this connection has been revoked, otherwise false. /// - IReadOnlyCollection IntegrationIds { get; } + bool? IsRevoked { get; } + /// + /// Gets a of integration parials. + /// + IReadOnlyCollection Integrations { get; } + /// + /// Gets whether the connection is verified. + /// + bool Verified { get; } + /// + /// Gets whether friend sync is enabled for this connection. + /// + bool FriendSync { get; } + /// + /// Gets whether activities related to this connection will be shown in presence updates. + /// + bool ShowActivity { get; } + /// + /// Visibility of this connection. + /// + ConnectionVisibility Visibility { get; } } } diff --git a/src/Discord.Net.Rest/API/Common/Connection.cs b/src/Discord.Net.Rest/API/Common/Connection.cs index bd8de3902..0a9940e23 100644 --- a/src/Discord.Net.Rest/API/Common/Connection.cs +++ b/src/Discord.Net.Rest/API/Common/Connection.cs @@ -7,14 +7,22 @@ namespace Discord.API { [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("type")] - public string Type { get; set; } [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } [JsonProperty("revoked")] - public bool Revoked { get; set; } - + public Optional Revoked { get; set; } [JsonProperty("integrations")] - public IReadOnlyCollection Integrations { get; set; } + public Optional> Integrations { get; set; } + [JsonProperty("verified")] + public bool Verified { get; set; } + [JsonProperty("friend_sync")] + public bool FriendSync { get; set; } + [JsonProperty("show_activity")] + public bool ShowActivity { get; set; } + [JsonProperty("visibility")] + public ConnectionVisibility Visibility { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/Integration.cs b/src/Discord.Net.Rest/API/Common/Integration.cs index 47d67e149..5a2b00001 100644 --- a/src/Discord.Net.Rest/API/Common/Integration.cs +++ b/src/Discord.Net.Rest/API/Common/Integration.cs @@ -5,6 +5,9 @@ namespace Discord.API { internal class Integration { + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("name")] @@ -14,18 +17,26 @@ namespace Discord.API [JsonProperty("enabled")] public bool Enabled { get; set; } [JsonProperty("syncing")] - public bool Syncing { get; set; } + public Optional Syncing { get; set; } [JsonProperty("role_id")] - public ulong RoleId { get; set; } + public Optional RoleId { get; set; } + [JsonProperty("enable_emoticons")] + public Optional EnableEmoticons { get; set; } [JsonProperty("expire_behavior")] - public ulong ExpireBehavior { get; set; } + public Optional ExpireBehavior { get; set; } [JsonProperty("expire_grace_period")] - public ulong ExpireGracePeriod { get; set; } + public Optional ExpireGracePeriod { get; set; } [JsonProperty("user")] - public User User { get; set; } + public Optional User { get; set; } [JsonProperty("account")] - public IntegrationAccount Account { get; set; } + public Optional Account { get; set; } [JsonProperty("synced_at")] - public DateTimeOffset SyncedAt { get; set; } + public Optional SyncedAt { get; set; } + [JsonProperty("subscriber_count")] + public Optional SubscriberAccount { get; set; } + [JsonProperty("revoked")] + public Optional Revoked { get; set; } + [JsonProperty("application")] + public Optional Application { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs index a8d33931c..6b8328074 100644 --- a/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs +++ b/src/Discord.Net.Rest/API/Common/IntegrationAccount.cs @@ -5,7 +5,7 @@ namespace Discord.API internal class IntegrationAccount { [JsonProperty("id")] - public ulong Id { get; set; } + public string Id { get; set; } [JsonProperty("name")] public string Name { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs b/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs new file mode 100644 index 000000000..4e07398b8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/IntegrationApplication.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class IntegrationApplication + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("summary")] + public string Summary { get; set; } + [JsonProperty("bot")] + public Optional Bot { get; set; } + } +} diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 5debea27e..c6ad6a9fb 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -49,7 +49,7 @@ namespace Discord.Rest public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); - return models.Select(RestConnection.Create).ToImmutableArray(); + return models.Select(model => RestConnection.Create(client, model)).ToImmutableArray(); } public static async Task GetInviteAsync(BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index f6d579d79..645e6711c 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1626,7 +1626,7 @@ namespace Discord.API #region Guild Integrations /// must not be equal to zero. - public async Task> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null) + public async Task> GetIntegrationsAsync(ulong guildId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); @@ -1634,47 +1634,14 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); } - /// and must not be equal to zero. - /// must not be . - public async Task CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(args.Id, 0, nameof(args.Id)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false); - } - public async Task DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); - } - public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, Rest.ModifyGuildIntegrationParams args, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); - Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior)); - Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/integrations/{integrationId}", args, ids, options: options).ConfigureAwait(false); - } - public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) + public async Task DeleteIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(integrationId, 0, nameof(integrationId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false); } #endregion diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 25f474dcc..7dbe20881 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -305,19 +305,15 @@ namespace Discord.Rest #endregion #region Integrations - public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, + public static async Task> GetIntegrationsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetGuildIntegrationsAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestGuildIntegration.Create(client, guild, x)).ToImmutableArray(); - } - public static async Task CreateIntegrationAsync(IGuild guild, BaseDiscordClient client, - ulong id, string type, RequestOptions options) - { - var args = new CreateGuildIntegrationParams(id, type); - var model = await client.ApiClient.CreateGuildIntegrationAsync(guild.Id, args, options).ConfigureAwait(false); - return RestGuildIntegration.Create(client, guild, model); + var models = await client.ApiClient.GetIntegrationsAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => RestIntegration.Create(client, guild, x)).ToImmutableArray(); } + public static async Task DeleteIntegrationAsync(IGuild guild, BaseDiscordClient client, ulong id, + RequestOptions options) => + await client.ApiClient.DeleteIntegrationAsync(guild.Id, id, options).ConfigureAwait(false); #endregion #region Interactions diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index e89096f00..d7ab65a55 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -720,10 +720,10 @@ namespace Discord.Rest #endregion #region Integrations - public Task> GetIntegrationsAsync(RequestOptions options = null) + public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); - public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) - => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null) + => GuildHelper.DeleteIntegrationAsync(this, Discord, id, options); #endregion #region Invites @@ -1370,11 +1370,11 @@ namespace Discord.Rest => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) - => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options) + => await DeleteIntegrationAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetInvitesAsync(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs deleted file mode 100644 index 9759e64d2..000000000 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Model = Discord.API.Integration; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGuildIntegration : RestEntity, IGuildIntegration - { - private long _syncedAtTicks; - - /// - public string Name { get; private set; } - /// - public string Type { get; private set; } - /// - public bool IsEnabled { get; private set; } - /// - public bool IsSyncing { get; private set; } - /// - public ulong ExpireBehavior { get; private set; } - /// - public ulong ExpireGracePeriod { get; private set; } - /// - public ulong GuildId { get; private set; } - /// - public ulong RoleId { get; private set; } - public RestUser User { get; private set; } - /// - public IntegrationAccount Account { get; private set; } - internal IGuild Guild { get; private set; } - - /// - public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); - - internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id) - : base(discord, id) - { - Guild = guild; - } - internal static RestGuildIntegration Create(BaseDiscordClient discord, IGuild guild, Model model) - { - var entity = new RestGuildIntegration(discord, guild, model.Id); - entity.Update(model); - return entity; - } - - internal void Update(Model model) - { - Name = model.Name; - Type = model.Type; - IsEnabled = model.Enabled; - IsSyncing = model.Syncing; - ExpireBehavior = model.ExpireBehavior; - ExpireGracePeriod = model.ExpireGracePeriod; - _syncedAtTicks = model.SyncedAt.UtcTicks; - - RoleId = model.RoleId; - User = RestUser.Create(Discord, model.User); - } - - public async Task DeleteAsync() - { - await Discord.ApiClient.DeleteGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); - } - public async Task ModifyAsync(Action func) - { - if (func == null) throw new NullReferenceException(nameof(func)); - - var args = new GuildIntegrationProperties(); - func(args); - var apiArgs = new API.Rest.ModifyGuildIntegrationParams - { - EnableEmoticons = args.EnableEmoticons, - ExpireBehavior = args.ExpireBehavior, - ExpireGracePeriod = args.ExpireGracePeriod - }; - var model = await Discord.ApiClient.ModifyGuildIntegrationAsync(GuildId, Id, apiArgs).ConfigureAwait(false); - - Update(model); - } - public async Task SyncAsync() - { - await Discord.ApiClient.SyncGuildIntegrationAsync(GuildId, Id).ConfigureAwait(false); - } - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; - - /// - IGuild IGuildIntegration.Guild - { - get - { - if (Guild != null) - return Guild; - throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); - } - } - /// - IUser IGuildIntegration.User => User; - } -} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs new file mode 100644 index 000000000..e92ecdded --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegration.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Integration; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestIntegration : RestEntity, IIntegration + { + private long? _syncedAtTicks; + + /// + public string Name { get; private set; } + /// + public string Type { get; private set; } + /// + public bool IsEnabled { get; private set; } + /// + public bool? IsSyncing { get; private set; } + /// + public ulong? RoleId { get; private set; } + /// + public bool? HasEnabledEmoticons { get; private set; } + /// + public IntegrationExpireBehavior? ExpireBehavior { get; private set; } + /// + public int? ExpireGracePeriod { get; private set; } + /// + IUser IIntegration.User => User; + /// + public IIntegrationAccount Account { get; private set; } + /// + public DateTimeOffset? SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); + /// + public int? SubscriberCount { get; private set; } + /// + public bool? IsRevoked { get; private set; } + /// + public IIntegrationApplication Application { get; private set; } + + internal IGuild Guild { get; private set; } + public RestUser User { get; private set; } + + internal RestIntegration(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, id) + { + Guild = guild; + } + internal static RestIntegration Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestIntegration(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Type = model.Type; + IsEnabled = model.Enabled; + + IsSyncing = model.Syncing.IsSpecified ? model.Syncing.Value : null; + RoleId = model.RoleId.IsSpecified ? model.RoleId.Value : null; + HasEnabledEmoticons = model.EnableEmoticons.IsSpecified ? model.EnableEmoticons.Value : null; + ExpireBehavior = model.ExpireBehavior.IsSpecified ? model.ExpireBehavior.Value : null; + ExpireGracePeriod = model.ExpireGracePeriod.IsSpecified ? model.ExpireGracePeriod.Value : null; + User = model.User.IsSpecified ? RestUser.Create(Discord, model.User.Value) : null; + Account = model.Account.IsSpecified ? RestIntegrationAccount.Create(model.Account.Value) : null; + SubscriberCount = model.SubscriberAccount.IsSpecified ? model.SubscriberAccount.Value : null; + IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null; + Application = model.Application.IsSpecified ? RestIntegrationApplication.Create(Discord, model.Application.Value) : null; + + _syncedAtTicks = model.SyncedAt.IsSpecified ? model.SyncedAt.Value.UtcTicks : null; + } + + public async Task DeleteAsync() + { + await Discord.ApiClient.DeleteIntegrationAsync(GuildId, Id).ConfigureAwait(false); + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; + + /// + public ulong GuildId { get; private set; } + + /// + IGuild IIntegration.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs new file mode 100644 index 000000000..6d83aa1f0 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationAccount.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.IntegrationAccount; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestIntegrationAccount : IIntegrationAccount + { + internal RestIntegrationAccount() { } + + public string Id { get; private set; } + + public string Name { get; private set; } + + internal static RestIntegrationAccount Create(Model model) + { + var entity = new RestIntegrationAccount(); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + model.Name = Name; + model.Id = Id; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs new file mode 100644 index 000000000..e532ac970 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Integrations/RestIntegrationApplication.cs @@ -0,0 +1,39 @@ +using Model = Discord.API.IntegrationApplication; + +namespace Discord.Rest +{ + /// + /// Represents a Rest-based implementation of . + /// + public class RestIntegrationApplication : RestEntity, IIntegrationApplication + { + public string Name { get; private set; } + + public string Icon { get; private set; } + + public string Description { get; private set; } + + public string Summary { get; private set; } + + public IUser Bot { get; private set; } + + internal RestIntegrationApplication(BaseDiscordClient discord, ulong id) + : base(discord, id) { } + + internal static RestIntegrationApplication Create(BaseDiscordClient discord, Model model) + { + var entity = new RestIntegrationApplication(discord, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + Icon = model.Icon.IsSpecified ? model.Icon.Value : null; + Description = model.Description; + Summary = model.Summary; + Bot = RestUser.Create(Discord, model.Bot.Value); + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index 1afb813c0..496279727 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using Model = Discord.API.Connection; namespace Discord.Rest @@ -9,28 +11,49 @@ namespace Discord.Rest public class RestConnection : IConnection { /// - public string Id { get; } + public string Id { get; private set; } /// - public string Type { get; } + public string Name { get; private set; } /// - public string Name { get; } + public string Type { get; private set; } /// - public bool IsRevoked { get; } + public bool? IsRevoked { get; private set; } /// - public IReadOnlyCollection IntegrationIds { get; } + public IReadOnlyCollection Integrations { get; private set; } + /// + public bool Verified { get; private set; } + /// + public bool FriendSync { get; private set; } + /// + public bool ShowActivity { get; private set; } + /// + public ConnectionVisibility Visibility { get; private set; } - internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection integrationIds) - { - Id = id; - Type = type; - Name = name; - IsRevoked = isRevoked; + internal BaseDiscordClient Discord { get; } - IntegrationIds = integrationIds; + internal RestConnection(BaseDiscordClient discord) { + Discord = discord; } - internal static RestConnection Create(Model model) + + internal static RestConnection Create(BaseDiscordClient discord, Model model) + { + var entity = new RestConnection(discord); + entity.Update(model); + return entity; + } + + internal void Update(Model model) { - return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray()); + Id = model.Id; + Name = model.Name; + Type = model.Type; + IsRevoked = model.Revoked.IsSpecified ? model.Revoked.Value : null; + Integrations = model.Integrations.IsSpecified ?model.Integrations.Value + .Select(intergration => RestIntegration.Create(Discord, null, intergration)).ToImmutableArray() : null; + Verified = model.Verified; + FriendSync = model.FriendSync; + ShowActivity = model.ShowActivity; + Visibility = model.Visibility; } /// @@ -40,6 +63,6 @@ namespace Discord.Rest /// Name of the connection. /// public override string ToString() => Name; - private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; + private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked.GetValueOrDefault() ? ", Revoked" : "")})"; } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs new file mode 100644 index 000000000..cf6e70ca6 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/IntegrationDeletedEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class IntegrationDeletedEvent + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationID { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index b8d3b6a10..c47591418 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -415,6 +415,32 @@ namespace Discord.WebSocket #endregion + #region Integrations + /// Fired when an integration is created. + public event Func IntegrationCreated + { + add { _integrationCreated.Add(value); } + remove { _integrationCreated.Remove(value); } + } + internal readonly AsyncEvent> _integrationCreated = new AsyncEvent>(); + + /// Fired when an integration is updated. + public event Func IntegrationUpdated + { + add { _integrationUpdated.Add(value); } + remove { _integrationUpdated.Remove(value); } + } + internal readonly AsyncEvent> _integrationUpdated = new AsyncEvent>(); + + /// Fired when an integration is deleted. + public event Func, Task> IntegrationDeleted + { + add { _integrationDeleted.Add(value); } + remove { _integrationDeleted.Remove(value); } + } + internal readonly AsyncEvent, Task>> _integrationDeleted = new AsyncEvent, Task>>(); + #endregion + #region Users /// Fired when a user joins a guild. public event Func UserJoined diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b692f0691..f33d89047 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2017,6 +2017,92 @@ namespace Discord.WebSocket break; #endregion + #region Integrations + case "INTEGRATION_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationCreated, nameof(IntegrationCreated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + // Integrations from Gateway should always have guild IDs specified. + if (!data.GuildId.IsSpecified) + return; + + var guild = State.GetGuild(data.GuildId.Value); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationUpdated, nameof(IntegrationUpdated), RestIntegration.Create(this, guild, data)).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + } + break; + case "INTEGRATION_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_integrationDeleted, nameof(IntegrationDeleted), guild, data.Id, data.ApplicationID).ConfigureAwait(false); + } + else + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + } + break; + #endregion + #region Users case "USER_UPDATE": { diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c4b756410..47bd57552 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -847,10 +847,10 @@ namespace Discord.WebSocket #endregion #region Integrations - public Task> GetIntegrationsAsync(RequestOptions options = null) + public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); - public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) - => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); + public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null) + => GuildHelper.DeleteIntegrationAsync(this, Discord, id, options); #endregion #region Interactions @@ -1888,11 +1888,11 @@ namespace Discord.WebSocket => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetIntegrationsAsync(RequestOptions options) + async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) - => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options) + => await DeleteIntegrationAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetInvitesAsync(RequestOptions options) From 91d8fabb70ad9008b4d205b3c38675289f2ca08e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sat, 26 Mar 2022 10:35:25 -0300 Subject: [PATCH 032/153] Fix: GuildPermissions.All not including newer permissions (#2209) --- src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 649944ede..4c3125907 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -13,7 +13,7 @@ namespace Discord /// Gets a that grants all guild permissions for webhook users. public static readonly GuildPermissions Webhook = new GuildPermissions(0b0_00000_0000000_0000000_0001101100000_000000); /// Gets a that grants all guild permissions. - public static readonly GuildPermissions All = new GuildPermissions(0b1_11111_1111111_1111111_1111111111111_111111); + public static readonly GuildPermissions All = new GuildPermissions(ulong.MaxValue); /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } From 73399459eacbc15187edf4dc9e26fd934b8a7fad Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sat, 26 Mar 2022 16:21:26 -0300 Subject: [PATCH 033/153] feature: add a way to remove type readers from the interaction/command service. (#2210) * Add remove methods * add inline docs Co-authored-by: Cenngo --- src/Discord.Net.Commands/CommandService.cs | 35 +++++++++++++ .../InteractionService.cs | 52 +++++++++++++++++++ src/Discord.Net.Interactions/Map/TypeMap.cs | 12 +++++ 3 files changed, 99 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d6dfc2fb7..57e0e430e 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -403,6 +403,41 @@ namespace Discord.Commands AddNullableTypeReader(type, reader); } } + + /// + /// Removes a type reader from the list of type readers. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// if the default readers for should be removed; otherwise . + /// The removed collection of type readers. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, bool isDefaultTypeReader, out IDictionary readers) + { + readers = new Dictionary(); + + if (isDefaultTypeReader) + { + var isSuccess = _defaultTypeReaders.TryRemove(type, out var result); + if (isSuccess) + readers.Add(result?.GetType(), result); + + return isSuccess; + } + else + { + var isSuccess = _typeReaders.TryRemove(type, out var result); + + if (isSuccess) + readers = result; + + return isSuccess; + } + } + internal bool HasDefaultTypeReader(Type type) { if (_defaultTypeReaders.ContainsKey(type)) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 927e39735..deb6fa931 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -905,9 +905,61 @@ namespace Discord.Interactions public void AddGenericTypeReader(Type targetType, Type readerType) => _typeReaderMap.AddGeneric(targetType, readerType); + /// + /// Removes a type reader for the type . + /// + /// The type to remove the readers from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(out TypeReader reader) + => TryRemoveTypeReader(typeof(T), out reader); + + /// + /// Removes a type reader for the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The reader if the resulting remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveTypeReader(Type type, out TypeReader reader) + => _typeReaderMap.TryRemoveConcrete(type, out reader); + + /// + /// Removes a generic type reader from the type . + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the readers from. + /// The removed readers type. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(out Type readerType) + => TryRemoveGenericTypeReader(typeof(T), out readerType); + + /// + /// Removes a generic type reader from the given type. + /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// + /// The type to remove the reader from. + /// The readers type if the remove operation was successful. + /// if the remove operation was successful; otherwise . + public bool TryRemoveGenericTypeReader(Type type, out Type readerType) + => _typeReaderMap.TryRemoveGeneric(type, out readerType); + /// /// Serialize an object using a into a to be placed in a Component CustomId. /// + /// + /// Removing a from the will not dereference the from the loaded module/command instances. + /// You need to reload the modules for the changes to take effect. + /// /// Type of the object to be serialized. /// Object to be serialized. /// Services that will be passed on to the . diff --git a/src/Discord.Net.Interactions/Map/TypeMap.cs b/src/Discord.Net.Interactions/Map/TypeMap.cs index ef1ef4a53..520ed7231 100644 --- a/src/Discord.Net.Interactions/Map/TypeMap.cs +++ b/src/Discord.Net.Interactions/Map/TypeMap.cs @@ -74,6 +74,18 @@ namespace Discord.Interactions _generics[targetType] = converterType; } + public bool TryRemoveConcrete(out TConverter converter) + => TryRemoveConcrete(typeof(TTarget), out converter); + + public bool TryRemoveConcrete(Type type, out TConverter converter) + => _concretes.TryRemove(type, out converter); + + public bool TryRemoveGeneric(out Type converterType) + => TryRemoveGeneric(typeof(TTarget), out converterType); + + public bool TryRemoveGeneric(Type targetType, out Type converterType) + => _generics.TryRemove(targetType, out converterType); + private Type GetMostSpecific(Type type) { if (_generics.TryGetValue(type, out var matching)) From c4131cfc8bc2aa22d2c133f766782b0ae407df36 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 31 Mar 2022 21:24:36 +0200 Subject: [PATCH 034/153] Fix: ShardedClients not pushing PresenceUpdates (#2219) --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index a361889c0..3a14692e0 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -449,6 +449,7 @@ namespace Discord.WebSocket client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); + client.PresenceUpdated += (user, oldPresence, newPresence) => _presenceUpdated.InvokeAsync(user, oldPresence, newPresence); client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState); client.VoiceServerUpdated += (server) => _voiceServerUpdatedEvent.InvokeAsync(server); From 1c680db2bafaf33bd1a660731f74fbfbbe6cb746 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Tue, 5 Apr 2022 00:11:15 +0300 Subject: [PATCH 035/153] add respondwithmodal methods to restinteractinmodulebase (#2227) --- .../Extensions/RestExtensions.cs | 25 ++++++++++++++ .../RestInteractionModuleBase.cs | 34 +++++++++++++++++++ .../Utilities/ApplicationCommandRestUtil.cs | 21 ++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/Discord.Net.Interactions/Extensions/RestExtensions.cs diff --git a/src/Discord.Net.Interactions/Extensions/RestExtensions.cs b/src/Discord.Net.Interactions/Extensions/RestExtensions.cs new file mode 100644 index 000000000..2641617e0 --- /dev/null +++ b/src/Discord.Net.Interactions/Extensions/RestExtensions.cs @@ -0,0 +1,25 @@ +using Discord.Interactions; +using System; + +namespace Discord.Rest +{ + public static class RestExtensions + { + /// + /// Respond to an interaction with a . + /// + /// Type of the implementation. + /// The interaction to respond to. + /// The request options for this request. + /// Serialized payload to be used to create a HTTP response. + public static string RespondWithModal(this RestInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + if (!ModalUtils.TryGet(out var modalInfo)) + throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + + var modal = modalInfo.ToModal(customId, modifyModal); + return interaction.RespondWithModal(modal, options); + } + } +} diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index a07614f7f..e83c91fef 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -65,5 +65,39 @@ namespace Discord.Interactions else await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } + + /// + /// Responds to the interaction with a modal. + /// + /// The modal to respond with. + /// The request options for this request. + /// A string that contains json to write back to the incoming http request. + /// + /// + protected override async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) + { + if (Context.Interaction is not RestInteraction restInteraction) + throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); + + var payload = restInteraction.RespondWithModal(modal, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); + } + + protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) + { + if (Context.Interaction is not RestInteraction restInteraction) + throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); + + var payload = restInteraction.RespondWithModal(customId, options); + + if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) + await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); + else + await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 46f0f4a4a..c2052b7c7 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -196,5 +196,26 @@ namespace Discord.Interactions }).ToList(), Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList() }; + + public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) + { + var builder = new ModalBuilder(modalInfo.Title, customId); + + foreach (var input in modalInfo.Components) + switch (input) + { + case TextInputComponentInfo textComponent: + builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, + textComponent.MaxLength, textComponent.IsRequired, textComponent.InitialValue); + break; + default: + throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); + } + + if(modifyModal is not null) + modifyModal(builder); + + return builder.Build(); + } } } From d2118f02fb422b88e0d2f4f88cc4003c56967dd3 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Tue, 5 Apr 2022 00:11:54 +0300 Subject: [PATCH 036/153] Adds a action delegate parameter to `RespondWithModalAsync()` for modifying the modal (#2226) * add modifyModal deleagate parameter to RespondWithModalAsync extension method * change the position of the new parameter to avoid introducing a breaking change --- .../Extensions/IDiscordInteractionExtensions.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 5c379cf42..8f0987661 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -10,9 +10,10 @@ namespace Discord.Interactions /// /// Type of the implementation. /// The interaction to respond to. + /// Delegate that can be used to modify the modal. /// The request options for this request. /// A task that represents the asynchronous operation of responding to the interaction. - public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null) + public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, RequestOptions options = null, Action modifyModal = null) where T : class, IModal { if (!ModalUtils.TryGet(out var modalInfo)) @@ -31,6 +32,9 @@ namespace Discord.Interactions throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class"); } + if (modifyModal is not null) + modifyModal(builder); + await interaction.RespondWithModalAsync(builder.Build(), options).ConfigureAwait(false); } } From a7449484772733d8bc122321ddebbd52e98b1b53 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 4 Apr 2022 23:14:36 +0200 Subject: [PATCH 037/153] feature: Global interaction post execution event. (#2213) * Init * Variable set to event * Put internal above private * Revert "Put internal above private" This reverts commit 77784f001faa58a90edf34edc195d6942ba9b451. * Revert "Variable set to event" This reverts commit 2b0cb81d76e2150a8d692028486aa1d402efe8a3. * Revert "Init" This reverts commit 9892ce7b51d8cf2952d4ae5b67f4a2e4c7917ae2. * Potentially improved approach --- .../InteractionService.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index deb6fa931..01fb8cc9d 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -24,6 +24,29 @@ namespace Discord.Interactions public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new (); + /// + /// Occurs when any type of interaction is executed. + /// + public event Func InteractionExecuted + { + add + { + SlashCommandExecuted += value; + ContextCommandExecuted += value; + ComponentCommandExecuted += value; + AutocompleteCommandExecuted += value; + ModalCommandExecuted += value; + } + remove + { + SlashCommandExecuted -= value; + ContextCommandExecuted -= value; + ComponentCommandExecuted -= value; + AutocompleteCommandExecuted -= value; + ModalCommandExecuted -= value; + } + } + /// /// Occurs when a Slash Command is executed. /// From 8522447c270b2d9a1409a8cf82070c0f326aac18 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 4 Apr 2022 23:16:13 +0200 Subject: [PATCH 038/153] Fix gateway interactions not running without bot scope. (#2217) * Init * Implement public channelId --- .../DiscordSocketClient.cs | 10 ++++++--- .../Entities/Interaction/SocketInteraction.cs | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f33d89047..b2da962ab 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2331,7 +2331,7 @@ namespace Discord.WebSocket SocketUser user = data.User.IsSpecified ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild.AddOrUpdateUser(data.Member.Value); + : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. SocketChannel channel = null; if(data.ChannelId.IsSpecified) @@ -2346,8 +2346,12 @@ namespace Discord.WebSocket } else { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; + if (guild != null) // The guild id is set, but the guild cannot be found as the bot scope is not set. + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + // The channel isnt required when responding to an interaction, so we can leave the channel null. } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 8b5bd9c32..5b2da04f5 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -19,13 +19,25 @@ namespace Discord.WebSocket /// Gets the this interaction was used in. /// /// - /// If the channel isn't cached or the bot doesn't have access to it then + /// If the channel isn't cached, the bot scope isn't used, or the bot doesn't have access to it then /// this property will be . /// public ISocketMessageChannel Channel { get; private set; } + /// + /// Gets the ID of the channel this interaction was used in. + /// + /// + /// This property is exposed in cases where the bot scope is not provided, so the channel entity cannot be retrieved. + ///
+ /// To get the channel, you can call + /// as this method makes a request for a if nothing was found in cache. + ///
+ public ulong? ChannelId { get; private set; } + /// /// Gets the who triggered this interaction. + /// This property will be if the bot scope isn't used. /// public SocketUser User { get; private set; } @@ -62,8 +74,6 @@ namespace Discord.WebSocket /// public bool IsDMInteraction { get; private set; } - private ulong? _channelId; - internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user) : base(client, id) { @@ -111,7 +121,7 @@ namespace Discord.WebSocket { IsDMInteraction = !model.GuildId.IsSpecified; - _channelId = model.ChannelId.ToNullable(); + ChannelId = model.ChannelId.ToNullable(); Data = model.Data.IsSpecified ? model.Data.Value @@ -396,12 +406,12 @@ namespace Discord.WebSocket if (Channel != null) return Channel; - if (!_channelId.HasValue) + if (!ChannelId.HasValue) return null; try { - return (IMessageChannel)await Discord.GetChannelAsync(_channelId.Value, options).ConfigureAwait(false); + return (IMessageChannel)await Discord.GetChannelAsync(ChannelId.Value, options).ConfigureAwait(false); } catch(HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) { return null; } // bot can't view that channel, return null instead of throwing. } From ce410513f4009bf14756ca8994abebc559ae0b60 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Mon, 4 Apr 2022 18:19:44 -0300 Subject: [PATCH 039/153] feature: build overrides (#2212) * add build overrides * override docs * add server submodule * add overrides to build step * remove testing api url Co-Authored-By: Quahu Co-authored-by: Quahu --- .gitmodules | 3 + Discord.Net.sln | 17 +- azure/deploy.yml | 1 + docs/faq/build_overrides/what-are-they.md | 41 +++ docs/faq/toc.yml | 2 + .../BuildOverrides.cs | 278 ++++++++++++++++++ .../Discord.Net.BuildOverrides.csproj | 20 ++ .../Discord.Net.BuildOverrides/IOverride.cs | 34 +++ .../OverrideContext.cs | 30 ++ overrides/Discord.Net.BuildOverrides | 1 + 10 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 docs/faq/build_overrides/what-are-they.md create mode 100644 experiment/Discord.Net.BuildOverrides/BuildOverrides.cs create mode 100644 experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj create mode 100644 experiment/Discord.Net.BuildOverrides/IOverride.cs create mode 100644 experiment/Discord.Net.BuildOverrides/OverrideContext.cs create mode 160000 overrides/Discord.Net.BuildOverrides diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..71d50ed3a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "overrides/Discord.Net.BuildOverrides"] + path = overrides/Discord.Net.BuildOverrides + url = https://github.com/discord-net/Discord.Net.BuildOverrides diff --git a/Discord.Net.sln b/Discord.Net.sln index fc68eb71c..544283b8b 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -34,7 +34,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_InteractionFramework", "sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_WebhookClient", "samples\WebhookClient\_WebhookClient.csproj", "{B61AAE66-15CC-40E4-873A-C23E697C3411}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDN", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7CF5621-7D36-433B-B337-5A2E3C101A71}" EndProject @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -258,6 +260,18 @@ Global {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x64.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.ActiveCfg = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Debug|x86.Build.0 = Debug|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|Any CPU.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU + {115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -279,6 +293,7 @@ Global {A23E46D2-1610-4AE5-820F-422D34810887} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/azure/deploy.yml b/azure/deploy.yml index 4742da3c8..d3460ad6c 100644 --- a/azure/deploy.yml +++ b/azure/deploy.yml @@ -8,6 +8,7 @@ steps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) + dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) displayName: Pack projects - task: NuGetCommand@2 diff --git a/docs/faq/build_overrides/what-are-they.md b/docs/faq/build_overrides/what-are-they.md new file mode 100644 index 000000000..f76fd6ddb --- /dev/null +++ b/docs/faq/build_overrides/what-are-they.md @@ -0,0 +1,41 @@ +--- +uid: FAQ.BuildOverrides.WhatAreThey +title: Build Overrides, What are they? +--- + +# Build Overrides + +Build overrides are a way for library developers to override the default behavior of the library on the fly. Adding them to your code is really simple. + +## Installing the package + +The build override package can be installed on nuget [here](TODO) or by using the package manager + +``` +PM> Install-Package Discord.Net.BuildOverrides +``` + +## Adding an override + +```cs +public async Task MainAsync() +{ + // hook into the log function + BuildOverrides.Log += (buildOverride, message) => + { + Console.WriteLine($"{buildOverride.Name}: {message}"); + return Task.CompletedTask; + }; + + // add your overrides + await BuildOverrides.AddOverrideAsync("example-override-name"); +} + +``` + +Overrides are normally built for specific problems, for example if someone is having an issue and we think we might have a fix then we can create a build override for them to test out the fix. + +## Security and Transparency + +Overrides can only be created and updated by library developers, you should only apply an override if a library developer askes you to. +Code for the overrides server and the overrides themselves can be found [here](https://github.com/discord-net/Discord.Net.BuildOverrides). \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 97e327aba..b727f5117 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -22,3 +22,5 @@ topicUid: FAQ.TextCommands.General - name: Legacy or Upgrade topicUid: FAQ.Legacy +- name: Build Overrides + topicUid: FAQ.BuildOverrides.WhatAreThey diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs new file mode 100644 index 000000000..fd15e5728 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs @@ -0,0 +1,278 @@ +using Discord.Overrides; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents an override that can be loaded. + /// + public sealed class Override + { + /// + /// Gets the ID of the override. + /// + public Guid Id { get; internal set; } + + /// + /// Gets the name of the override. + /// + public string Name { get; internal set; } + + /// + /// Gets the description of the override. + /// + public string Description { get; internal set; } + + /// + /// Gets the date this override was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the date the override was last modified. + /// + public DateTimeOffset LastUpdated { get; internal set; } + + internal static Override FromJson(string json) + { + var result = new Override(); + + using(var textReader = new StringReader(json)) + using(var reader = new JsonTextReader(textReader)) + { + var obj = JObject.ReadFrom(reader); + result.Id = obj["id"].ToObject(); + result.Name = obj["name"].ToObject(); + result.Description = obj["description"].ToObject(); + result.CreatedAt = obj["created_at"].ToObject(); + result.LastUpdated = obj["last_updated"].ToObject(); + } + + return result; + } + } + + /// + /// Represents a loaded override instance. + /// + public sealed class LoadedOverride + { + /// + /// Gets the aseembly containing the overrides definition. + /// + public Assembly Assembly { get; internal set; } + + /// + /// Gets an instance of the override. + /// + public IOverride Instance { get; internal set; } + + /// + /// Gets the overrides type. + /// + public Type Type { get; internal set; } + } + + public sealed class BuildOverrides + { + /// + /// Fired when an override logs a message. + /// + public static event Func Log + { + add => _logEvents.Add(value); + remove => _logEvents.Remove(value); + + } + + /// + /// Gets a read-only dictionary containing the currently loaded overrides. + /// + public IReadOnlyDictionary> LoadedOverrides + => _loadedOverrides.Select(x => new KeyValuePair> (x.Key, x.Value)).ToDictionary(x => x.Key, x => x.Value); + + private static AssemblyLoadContext _overrideDomain; + private static List> _logEvents = new(); + private static ConcurrentDictionary> _loadedOverrides = new ConcurrentDictionary>(); + + private const string ApiUrl = "https://overrides.discordnet.dev"; + + static BuildOverrides() + { + _overrideDomain = new AssemblyLoadContext("Discord.Net.Overrides.Runtime"); + + _overrideDomain.Resolving += _overrideDomain_Resolving; + } + + /// + /// Gets details about a specific override. + /// + /// + /// Note: This method does not load an override, it simply retrives the info about it. + /// + /// The name of the override to get. + /// + /// A task representing the asynchronous get operation. The tasks result is an + /// if it exists; otherwise . + /// + public static async Task GetOverrideAsync(string name) + { + using (var client = new HttpClient()) + { + var result = await client.GetAsync($"{ApiUrl}/override/{name}"); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + + return Override.FromJson(content); + } + else + return null; + } + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The name of the override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task AddOverrideAsync(string name) + { + var ovrride = await GetOverrideAsync(name); + + if (ovrride == null) + return false; + + return await AddOverrideAsync(ovrride); + } + + /// + /// Adds an override to the current Discord.Net instance. + /// + /// + /// The override initialization is non-blocking, any errors that occor within + /// the overrides initialization procedure will be sent in the event. + /// + /// The override to add. + /// + /// A task representing the asynchronous add operaton. The tasks result is a boolean + /// determining if the add operation was successful. + /// + public static async Task AddOverrideAsync(Override ovrride) + { + // download it + var ms = new MemoryStream(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync($"{ApiUrl}/override/download/{ovrride.Id}"); + + if (!result.IsSuccessStatusCode) + return false; + + await (await result.Content.ReadAsStreamAsync()).CopyToAsync(ms); + } + + ms.Position = 0; + + // load the assembly + //var test = Assembly.Load(ms.ToArray()); + var asm = _overrideDomain.LoadFromStream(ms); + + // find out IOverride + var overrides = asm.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(IOverride))); + + List loaded = new(); + + var context = new OverrideContext((m) => HandleLog(ovrride, m), ovrride); + + foreach (var ovr in overrides) + { + var inst = (IOverride)Activator.CreateInstance(ovr); + + inst.RegisterPackageLookupHandler((s) => + { + return GetDependencyAsync(ovrride.Id, s); + }); + + _ = Task.Run(async () => + { + try + { + await inst.InitializeAsync(context); + } + catch (Exception x) + { + HandleLog(ovrride, $"Failed to initialize build override: {x}"); + } + }); + + loaded.Add(new LoadedOverride() + { + Assembly = asm, + Instance = inst, + Type = ovr + }); + } + + return _loadedOverrides.AddOrUpdate(ovrride, loaded, (_, __) => loaded) != null; + } + + internal static void HandleLog(Override ovr, string msg) + { + _ = Task.Run(async () => + { + foreach (var item in _logEvents) + { + await item.Invoke(ovr, msg).ConfigureAwait(false); + } + }); + } + + private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2) + { + // resolve the override id + var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName)); + + return GetDependencyAsync(v.Key.Id, $"{arg2}").GetAwaiter().GetResult(); + } + + private static async Task GetDependencyAsync(Guid id, string name) + { + using(var client = new HttpClient()) + { + var result = await client.PostAsync($"{ApiUrl}/override/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json")); + + if (!result.IsSuccessStatusCode) + throw new Exception("Failed to get dependency"); + + using(var ms = new MemoryStream()) + { + var innerStream = await result.Content.ReadAsStreamAsync(); + await innerStream.CopyToAsync(ms); + ms.Position = 0; + return _overrideDomain.LoadFromStream(ms); + } + } + } + } +} diff --git a/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj new file mode 100644 index 000000000..25b1c40b0 --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/Discord.Net.BuildOverrides.csproj @@ -0,0 +1,20 @@ + + + + 9.0 + Discord.Net.BuildOverrides + Discord.BuildOverrides + A Discord.Net extension adding a way to add build overrides for testing. + net6.0;net5.0; + net6.0;net5.0; + + + + + + + + + + + diff --git a/experiment/Discord.Net.BuildOverrides/IOverride.cs b/experiment/Discord.Net.BuildOverrides/IOverride.cs new file mode 100644 index 000000000..17327ae2c --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/IOverride.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Overrides +{ + /// + /// Represents a generic build override for Discord.Net + /// + public interface IOverride + { + /// + /// Initializes the override. + /// + /// + /// This method is called by the class + /// and should not be called externally from it. + /// + /// Context used by an override to initialize. + /// + /// A task representing the asynchronous initialization operation. + /// + Task InitializeAsync(OverrideContext context); + + /// + /// Registers a callback to load a dependency for this override. + /// + /// The callback to load an external dependency. + void RegisterPackageLookupHandler(Func> func); + } +} diff --git a/experiment/Discord.Net.BuildOverrides/OverrideContext.cs b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs new file mode 100644 index 000000000..1e88be74a --- /dev/null +++ b/experiment/Discord.Net.BuildOverrides/OverrideContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Overrides +{ + /// + /// Represents context thats passed to an override in the initialization step. + /// + public sealed class OverrideContext + { + /// + /// A callback used to log messages. + /// + public Action Log { get; private set; } + + /// + /// The info about the override. + /// + public Override Info { get; private set; } + + internal OverrideContext(Action log, Override info) + { + Log = log; + Info = info; + } + } +} diff --git a/overrides/Discord.Net.BuildOverrides b/overrides/Discord.Net.BuildOverrides new file mode 160000 index 000000000..9b2be5597 --- /dev/null +++ b/overrides/Discord.Net.BuildOverrides @@ -0,0 +1 @@ +Subproject commit 9b2be5597468329090015fa1b2775815b20be440 From 0439437a65e5a4ef4087c2a5115824b7908dfa99 Mon Sep 17 00:00:00 2001 From: TricolorHen061 <55330531+TricolorHen061@users.noreply.github.com> Date: Mon, 4 Apr 2022 14:20:09 -0700 Subject: [PATCH 040/153] Fix small typo in modal example (#2216) --- docs/guides/int_framework/samples/intro/modal.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs index af72fe04e..65cc81abf 100644 --- a/docs/guides/int_framework/samples/intro/modal.cs +++ b/docs/guides/int_framework/samples/intro/modal.cs @@ -20,7 +20,7 @@ public class FoodModal : IModal // Responds to the modal. [ModalInteraction("food_menu")] -public async Task ModalResponce(FoodModal modal) +public async Task ModalResponse(FoodModal modal) { // Build the message to send. string message = "hey @everyone, I just learned " + From e38104bb3263d837b89c1449e9ef39f1e25a7e15 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 4 Apr 2022 23:21:11 +0200 Subject: [PATCH 041/153] feature: Make bidirectional formatting optional (#2204) * Init * Clearing up comment on config entry. * Update user entities to remove storage of the setting Co-authored-by: Quin Lynch --- src/Discord.Net.Core/DiscordConfig.cs | 10 ++++++++++ src/Discord.Net.Core/Format.cs | 9 ++++++--- src/Discord.Net.Rest/BaseDiscordClient.cs | 2 ++ src/Discord.Net.Rest/Entities/Users/RestUser.cs | 6 ++++-- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 34bfc5e62..006a1ca17 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -187,5 +187,15 @@ namespace Discord /// This will still require a stable clock on your system. /// public bool UseInteractionSnowflakeDate { get; set; } = true; + + /// + /// Gets or sets if the Rest/Socket user override formats the string in respect to bidirectional unicode. + /// + /// + /// By default, the returned value will be "?Discord?#1234", to work with bidirectional usernames. + ///
+ /// If set to , this value will be "Discord#1234". + ///
+ public bool FormatUsersInBidirectionalUnicode { get; set; } = true; } } diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index a5951aa73..dc2a06540 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -107,13 +107,16 @@ namespace Discord } /// - /// Formats a user's username + discriminator while maintaining bidirectional unicode + /// Formats a user's username + discriminator. /// + /// To format the string in bidirectional unicode or not /// The user whos username and discriminator to format /// The username + discriminator - public static string UsernameAndDiscriminator(IUser user) + public static string UsernameAndDiscriminator(IUser user, bool doBidirectional) { - return $"\u2066{user.Username}\u2069#{user.Discriminator}"; + return doBidirectional + ? $"\u2066{user.Username}\u2069#{user.Discriminator}" + : $"{user.Username}#{user.Discriminator}"; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 2bf08e3c7..75f477c7c 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -36,6 +36,7 @@ namespace Discord.Rest /// public TokenType TokenType => ApiClient.AuthTokenType; internal bool UseInteractionSnowflakeDate { get; private set; } + internal bool FormatUsersInBidirectionalUnicode { get; private set; } /// Creates a new REST-only Discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) @@ -49,6 +50,7 @@ namespace Discord.Rest _isFirstLogin = config.DisplayInitialLog; UseInteractionSnowflakeDate = config.UseInteractionSnowflakeDate; + FormatUsersInBidirectionalUnicode = config.FormatUsersInBidirectionalUnicode; ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 70f990fe7..dfdb53815 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -129,8 +129,10 @@ namespace Discord.Rest /// /// A string that resolves to Username#Discriminator of the user. /// - public override string ToString() => Format.UsernameAndDiscriminator(this); - private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() + => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); + + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; #endregion #region IUser diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 35121d666..d70e61739 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -117,8 +117,8 @@ namespace Discord.WebSocket /// /// The full name of the user. /// - public override string ToString() => Format.UsernameAndDiscriminator(this); - private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this)} ({Id}{(IsBot ? ", Bot" : "")})"; + public override string ToString() => Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode); + private string DebuggerDisplay => $"{Format.UsernameAndDiscriminator(this, Discord.FormatUsersInBidirectionalUnicode)} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; } } From bfd0d9bede3993ab2502c54f0f5dfe528802e494 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:17:18 -0300 Subject: [PATCH 042/153] fix: GuildMemberUpdated cacheable before entity incorrect (#2225) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b2da962ab..aaef4656a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1311,7 +1311,7 @@ namespace Discord.WebSocket else { user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(user, user.Id, true, () => null); + var cacheableBefore = new Cacheable(null, user.Id, false, () => null); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } From d1cf1bf02daa91d26cabdc0264e17f56ea037d9a Mon Sep 17 00:00:00 2001 From: clarotech Date: Tue, 5 Apr 2022 18:12:06 +0100 Subject: [PATCH 043/153] Correct minor typo (#2228) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1dfc41688..3e18513c2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,7 +67,7 @@ Being interactions, they are handled as SocketInteractions. Creating and receivi - Find out more about slash commands in the [Slash Command Guides](xref:Guides.SlashCommands.Intro) -#### Context Message & User Ccommands +#### Context Message & User Commands These commands can be pointed at messages and users, in custom application tabs. Being interactions as well, they are able to be handled just like slash commands. They do not have options however. From d8757a5afaed78b75c5705c5688d1e151c348d5d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:13:16 +0200 Subject: [PATCH 044/153] feature: Update bans to support pagination (#2223) * Cacheless impl * Ignore cache impl * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Implement xmldoc consistency Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordConfig.cs | 7 ++ .../Entities/Channels/Direction.cs | 10 +-- .../Entities/Guilds/IGuild.cs | 65 +++++++++++++++++-- .../API/Rest/GetGuildBansParams.cs | 9 +++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 20 +++++- .../Entities/Guilds/GuildHelper.cs | 50 ++++++++++++-- .../Entities/Guilds/RestGuild.cs | 37 ++++++----- .../Entities/Guilds/SocketGuild.cs | 34 ++++++---- 8 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 006a1ca17..067c55225 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -97,6 +97,13 @@ namespace Discord /// public const int MaxUsersPerBatch = 1000; /// + /// Returns the max bans allowed to be in a request. + /// + /// + /// The maximum number of bans that can be gotten per-batch. + /// + public const int MaxBansPerBatch = 1000; + /// /// Returns the max users allowed to be in a request for guild event users. /// /// diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index efdf4ff42..4149617d8 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,10 +1,10 @@ namespace Discord { /// - /// Specifies the direction of where message(s) should be retrieved from. + /// Specifies the direction of where entities (e.g. bans/messages) should be retrieved from. /// /// - /// This enum is used to specify the direction for retrieving messages. + /// This enum is used to specify the direction for retrieving entities. /// /// At the time of writing, is not yet implemented into /// . @@ -15,15 +15,15 @@ namespace Discord public enum Direction { /// - /// The message(s) should be retrieved before a message. + /// The entity(s) should be retrieved before an entity. /// Before, /// - /// The message(s) should be retrieved after a message. + /// The entity(s) should be retrieved after an entity. /// After, /// - /// The message(s) should be retrieved around a message. + /// The entity(s) should be retrieved around an entity. /// Around } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b4625abbf..4706b629e 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -409,17 +409,70 @@ namespace Discord /// A task that represents the asynchronous leave operation. /// Task LeaveAsync(RequestOptions options = null); - /// - /// Gets a collection of all users banned in this guild. + /// Gets amount of bans from the guild ordered by user ID. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The amount of bans to get from the guild. /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The ID of the user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. /// - Task> GetBansAsync(RequestOptions options = null); + IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); /// /// Gets a ban object for a banned user. /// diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs new file mode 100644 index 000000000..6a1e430c3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs @@ -0,0 +1,9 @@ +namespace Discord.API.Rest +{ + internal class GetGuildBansParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 645e6711c..3b829ee17 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1545,13 +1545,29 @@ namespace Discord.API #endregion #region Guild Bans - public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) + public async Task> GetGuildBansAsync(ulong guildId, GetGuildBansParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxBansPerBatch, nameof(args.Limit)); options = RequestOptions.CreateOrClone(options); + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxBansPerBatch); + ulong? relativeId = args.RelativeUserId.IsSpecified ? args.RelativeUserId.Value : (ulong?)null; + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch + { + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; var ids = new BucketIds(guildId: guildId); - return await SendAsync>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"guilds/{guildId}/bans?limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () => $"guilds/{guildId}/bans?limit={limit}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } public async Task GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7dbe20881..469e93db4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -142,12 +142,54 @@ namespace Discord.Rest #endregion #region Bans - public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, - RequestOptions options) + public static IAsyncEnumerable> GetBansAsync(IGuild guild, BaseDiscordClient client, + ulong? fromUserId, Direction dir, int limit, RequestOptions options) { - var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); + if (dir == Direction.Around && limit > DiscordConfig.MaxBansPerBatch) + { + int around = limit / 2; + if (fromUserId.HasValue) + return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) + .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); + else + return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); + } + + return new PagedAsyncEnumerable( + DiscordConfig.MaxBansPerBatch, + async (info, ct) => + { + var args = new GetGuildBansParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + + var models = await client.ApiClient.GetGuildBansAsync(guild.Id, args, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + + foreach (var model in models) + builder.Add(RestBan.Create(client, model)); + + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.User.Id); + else + info.Position = lastPage.Max(x => x.User.Id); + return true; + }, + start: fromUserId, + count: limit + ); } + public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index d7ab65a55..92d598466 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -333,17 +333,18 @@ namespace Discord.Rest #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); /// /// Gets a ban object for a banned user. /// @@ -1193,22 +1194,24 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Roles => Roles; IReadOnlyCollection IGuild.Stickers => Stickers; - /// async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); - /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); - /// async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); - /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 47bd57552..49d2cd3bd 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -621,17 +621,19 @@ namespace Discord.WebSocket #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); + /// /// Gets a ban object for a banned user. /// @@ -1810,8 +1812,14 @@ namespace Discord.WebSocket async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); From 53ab9f3b16868226a8ba22f206f90cf6dad426fb Mon Sep 17 00:00:00 2001 From: Duke <40759437+dukesteen@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:18:25 +0200 Subject: [PATCH 045/153] MediatR Guide + sample (#2218) * Add guide for MediatR * Add sample for MediatR * Fix exposed token in program.cs * Fix review points * Remove newline in MediatrDiscordEventListener.cs --- .../other_libs/images/mediatr_output.png | Bin 0 -> 24076 bytes docs/guides/other_libs/mediatr.md | 70 +++++++++++++++++ .../samples/MediatrConfiguringDI.cs | 1 + .../MediatrCreatingMessageNotification.cs | 16 ++++ .../samples/MediatrDiscordEventListener.cs | 46 +++++++++++ .../samples/MediatrMessageReceivedHandler.cs | 17 ++++ .../samples/MediatrStartListener.cs | 4 + docs/guides/toc.yml | 2 + samples/MediatRSample/MediatRSample.sln | 16 ++++ .../MediatRSample/DiscordEventListener.cs | 48 ++++++++++++ .../Handlers/MessageReceivedHandler.cs | 14 ++++ .../MediatRSample/MediatRSample.csproj | 20 +++++ .../MessageReceivedNotification.cs | 14 ++++ .../Notifications/ReadyNotification.cs | 13 ++++ .../MediatRSample/MediatRSample/Program.cs | 73 ++++++++++++++++++ 15 files changed, 354 insertions(+) create mode 100644 docs/guides/other_libs/images/mediatr_output.png create mode 100644 docs/guides/other_libs/mediatr.md create mode 100644 docs/guides/other_libs/samples/MediatrConfiguringDI.cs create mode 100644 docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs create mode 100644 docs/guides/other_libs/samples/MediatrDiscordEventListener.cs create mode 100644 docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs create mode 100644 docs/guides/other_libs/samples/MediatrStartListener.cs create mode 100644 samples/MediatRSample/MediatRSample.sln create mode 100644 samples/MediatRSample/MediatRSample/DiscordEventListener.cs create mode 100644 samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs create mode 100644 samples/MediatRSample/MediatRSample/MediatRSample.csproj create mode 100644 samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs create mode 100644 samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs create mode 100644 samples/MediatRSample/MediatRSample/Program.cs diff --git a/docs/guides/other_libs/images/mediatr_output.png b/docs/guides/other_libs/images/mediatr_output.png new file mode 100644 index 0000000000000000000000000000000000000000..801809313bb553421b5902fd23072b075960bf48 GIT binary patch literal 24076 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 wUO70h+~}B`qtEBpCCdK)AJ(`PJ$r>ci` interface provided by MediatR, this tells MediatR to dispatch `MessageReceivedNotification` notifications to this handler class. + +> [!NOTE] +> You can create as many notification handlers for the same notification as you desire. That's the beauty of MediatR! + +## Testing + +To test if we have successfully implemented MediatR, we can start up the bot and send a message to a server the bot is in. It should print out the message we defined earlier in our `MessageReceivedHandler`. + +![MediatR output](images/mediatr_output.png) + +## Adding more event types + +To add more event types you can follow these steps: + +1. Create a new notification class for the event. it should contain all of the parameters that the event would send. (Ex: the `MessageReceived` event takes one `SocketMessage` as an argument. The notification class should also map this argument) +2. Register the event in your `DiscordEventListener` class. +3. Create a notification handler for your new notification. diff --git a/docs/guides/other_libs/samples/MediatrConfiguringDI.cs b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs new file mode 100644 index 000000000..3bef7bd76 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs @@ -0,0 +1 @@ +.AddMediatR(typeof(Bot)) diff --git a/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs new file mode 100644 index 000000000..449c96eb4 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs @@ -0,0 +1,16 @@ +// MessageReceivedNotification.cs + +using Discord.WebSocket; +using MediatR; + +namespace MediatRSample.Notifications; + +public class MessageReceivedNotification : INotification +{ + public MessageReceivedNotification(SocketMessage message) + { + Message = message ?? throw new ArgumentNullException(nameof(message)); + } + + public SocketMessage Message { get; } +} diff --git a/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs new file mode 100644 index 000000000..09583c3e9 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs @@ -0,0 +1,46 @@ +// DiscordEventListener.cs + +using Discord.WebSocket; +using MediatR; +using MediatRSample.Notifications; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; + +namespace MediatRSample; + +public class DiscordEventListener +{ + private readonly CancellationToken _cancellationToken; + + private readonly DiscordSocketClient _client; + private readonly IServiceScopeFactory _serviceScope; + + public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope) + { + _client = client; + _serviceScope = serviceScope; + _cancellationToken = new CancellationTokenSource().Token; + } + + private IMediator Mediator + { + get + { + var scope = _serviceScope.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } + } + + public async Task StartAsync() + { + _client.MessageReceived += OnMessageReceivedAsync; + + await Task.CompletedTask; + } + + private Task OnMessageReceivedAsync(SocketMessage arg) + { + return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + } +} diff --git a/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs new file mode 100644 index 000000000..1ab2491e2 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs @@ -0,0 +1,17 @@ +// MessageReceivedHandler.cs + +using System; +using MediatR; +using MediatRSample.Notifications; + +namespace MediatRSample; + +public class MessageReceivedHandler : INotificationHandler +{ + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})"); + + // Your implementation + } +} diff --git a/docs/guides/other_libs/samples/MediatrStartListener.cs b/docs/guides/other_libs/samples/MediatrStartListener.cs new file mode 100644 index 000000000..72a54bf25 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrStartListener.cs @@ -0,0 +1,4 @@ +// Program.cs + +var listener = services.GetRequiredService(); +await listener.StartAsync(); diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index b1a6b4721..af0a8e2b4 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -115,6 +115,8 @@ topicUid: Guides.OtherLibs.Serilog - name: EFCore topicUid: Guides.OtherLibs.EFCore + - name: MediatR + topicUid: Guides.OtherLibs.MediatR - name: Emoji topicUid: Guides.Emoji - name: Voice diff --git a/samples/MediatRSample/MediatRSample.sln b/samples/MediatRSample/MediatRSample.sln new file mode 100644 index 000000000..d0599ae26 --- /dev/null +++ b/samples/MediatRSample/MediatRSample.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/MediatRSample/MediatRSample/DiscordEventListener.cs b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs new file mode 100644 index 000000000..dec342773 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/DiscordEventListener.cs @@ -0,0 +1,48 @@ +using Discord.WebSocket; +using MediatR; +using MediatRSample.Notifications; +using Microsoft.Extensions.DependencyInjection; + +namespace MediatRSample; + +public class DiscordEventListener +{ + private readonly CancellationToken _cancellationToken; + + private readonly DiscordSocketClient _client; + private readonly IServiceScopeFactory _serviceScope; + + public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope) + { + _client = client; + _serviceScope = serviceScope; + _cancellationToken = new CancellationTokenSource().Token; + } + + private IMediator Mediator + { + get + { + var scope = _serviceScope.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } + } + + public Task StartAsync() + { + _client.Ready += OnReadyAsync; + _client.MessageReceived += OnMessageReceivedAsync; + + return Task.CompletedTask; + } + + private Task OnMessageReceivedAsync(SocketMessage arg) + { + return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + } + + private Task OnReadyAsync() + { + return Mediator.Publish(ReadyNotification.Default, _cancellationToken); + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs new file mode 100644 index 000000000..5cae3f267 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Handlers/MessageReceivedHandler.cs @@ -0,0 +1,14 @@ +using MediatR; +using MediatRSample.Notifications; + +namespace MediatRSample.Handlers; + +public class MessageReceivedHandler : INotificationHandler +{ + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})"); + + // Your implementation + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/MediatRSample.csproj b/samples/MediatRSample/MediatRSample/MediatRSample.csproj new file mode 100644 index 000000000..4e9d01c8c --- /dev/null +++ b/samples/MediatRSample/MediatRSample/MediatRSample.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + Linux + + + + + + + + + + + + diff --git a/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs new file mode 100644 index 000000000..610b4a0a5 --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Notifications/MessageReceivedNotification.cs @@ -0,0 +1,14 @@ +using Discord.WebSocket; +using MediatR; + +namespace MediatRSample.Notifications; + +public class MessageReceivedNotification : INotification +{ + public MessageReceivedNotification(SocketMessage message) + { + Message = message ?? throw new ArgumentNullException(nameof(message)); + } + + public SocketMessage Message { get; } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs new file mode 100644 index 000000000..bafa6c10b --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Notifications/ReadyNotification.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace MediatRSample.Notifications; + +public class ReadyNotification : INotification +{ + public static readonly ReadyNotification Default + = new(); + + private ReadyNotification() + { + } +} \ No newline at end of file diff --git a/samples/MediatRSample/MediatRSample/Program.cs b/samples/MediatRSample/MediatRSample/Program.cs new file mode 100644 index 000000000..96b393e5d --- /dev/null +++ b/samples/MediatRSample/MediatRSample/Program.cs @@ -0,0 +1,73 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; + +namespace MediatRSample; + +public class Bot +{ + private static ServiceProvider ConfigureServices() + { + return new ServiceCollection() + .AddMediatR(typeof(Bot)) + .AddSingleton(new DiscordSocketClient(new DiscordSocketConfig + { + AlwaysDownloadUsers = true, + MessageCacheSize = 100, + GatewayIntents = GatewayIntents.AllUnprivileged, + LogLevel = LogSeverity.Info + })) + .AddSingleton() + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .BuildServiceProvider(); + } + + public static async Task Main() + { + await new Bot().RunAsync(); + } + + private async Task RunAsync() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + await using var services = ConfigureServices(); + + var client = services.GetRequiredService(); + client.Log += LogAsync; + + var listener = services.GetRequiredService(); + await listener.StartAsync(); + + await client.LoginAsync(TokenType.Bot, "YOUR_TOKEN_HERE"); + await client.StartAsync(); + + await Task.Delay(Timeout.Infinite); + } + + private static Task LogAsync(LogMessage message) + { + var severity = message.Severity switch + { + LogSeverity.Critical => LogEventLevel.Fatal, + LogSeverity.Error => LogEventLevel.Error, + LogSeverity.Warning => LogEventLevel.Warning, + LogSeverity.Info => LogEventLevel.Information, + LogSeverity.Verbose => LogEventLevel.Verbose, + LogSeverity.Debug => LogEventLevel.Debug, + _ => LogEventLevel.Information + }; + + Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); + + return Task.CompletedTask; + } +} From d3a532f0132f7e35115811d938f2abc90fd68c8d Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 5 Apr 2022 15:20:57 -0300 Subject: [PATCH 046/153] update build overrides url --- experiment/Discord.Net.BuildOverrides/BuildOverrides.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs index fd15e5728..54b56cc60 100644 --- a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs +++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs @@ -130,7 +130,7 @@ namespace Discord { using (var client = new HttpClient()) { - var result = await client.GetAsync($"{ApiUrl}/override/{name}"); + var result = await client.GetAsync($"{ApiUrl}/overrides/{name}"); if (result.IsSuccessStatusCode) { @@ -184,7 +184,7 @@ namespace Discord using (var client = new HttpClient()) { - var result = await client.GetAsync($"{ApiUrl}/override/download/{ovrride.Id}"); + var result = await client.GetAsync($"{ApiUrl}/overrides/download/{ovrride.Id}"); if (!result.IsSuccessStatusCode) return false; @@ -260,7 +260,7 @@ namespace Discord { using(var client = new HttpClient()) { - var result = await client.PostAsync($"{ApiUrl}/override/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json")); + var result = await client.PostAsync($"{ApiUrl}/overrides/{id}/dependency", new StringContent($"{{ \"info\": \"{name}\"}}", Encoding.UTF8, "application/json")); if (!result.IsSuccessStatusCode) throw new Exception("Failed to get dependency"); From 99928747032f1bc72d33d3b0b8dbb28d969a1625 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 5 Apr 2022 16:21:33 -0300 Subject: [PATCH 047/153] meta: 3.5.0 --- CHANGELOG.md | 26 +++++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6884d3564..3e4de065c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [3.5.0] - 2022-04-05 + +### Added +- #2204 Added config option for bidirectional formatting of usernames (e38104b) +- #2210 Add a way to remove type readers from the interaction/command service. (7339945) +- #2213 Add global interaction post execution event. (a744948) +- #2223 Add ban pagination support (d8757a5) +- #2201 Add missing interface methods to IComponentInteraction (741ed80) +- #2226 Add an action delegate parameter to `RespondWithModalAsync()` for modifying the modal (d2118f0) +- #2227 Add RespondWithModal methods to RestInteractinModuleBase (1c680db) + +### Fixed +- #2168 Fix Integration model from GuildIntegration and added INTEGRATION gateway events (305d7f9) +- #2187 Fix modal response failing (d656722) +- #2188 Fix serialization error on thread creation timestamp. (d48a7bd) +- #2209 Fix GuildPermissions.All not including newer permissions (91d8fab) +- #2219 Fix ShardedClients not pushing PresenceUpdates (c4131cf) +- #2225 Fix GuildMemberUpdated cacheable `before` entity being incorrect (bfd0d9b) +- #2217 Fix gateway interactions not running without bot scope. (8522447) + +### Misc +- #2193 Update GuildMemberUpdated comment regarding presence (82473bc) +- #2206 Fixed typo (c286b99) +- #2216 Fix small typo in modal example (0439437) +- #2228 Correct minor typo (d1cf1bf) + ## [3.4.1] - 2022-03-9 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index 187ff9d75..e50e6eceb 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.4.1 + 3.5.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 3b7ef582b..2a4ee2867 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.4.1", + "_appFooter": "Discord.Net (c) 2015-2022 3.5.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 996e9bae9..d79e9a24a 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.4.1$suffix$ + 3.5.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 8eec6a00acdca8a75f0352145cb3048a27ea46ea Mon Sep 17 00:00:00 2001 From: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Date: Mon, 18 Apr 2022 02:51:40 -0400 Subject: [PATCH 048/153] Fix log severity mapping for guide sample (#2249) --- docs/guides/other_libs/samples/ModifyLogMethod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs index b4870cfd1..0f7c11daf 100644 --- a/docs/guides/other_libs/samples/ModifyLogMethod.cs +++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs @@ -6,8 +6,8 @@ private static async Task LogAsync(LogMessage message) LogSeverity.Error => LogEventLevel.Error, LogSeverity.Warning => LogEventLevel.Warning, LogSeverity.Info => LogEventLevel.Information, - LogSeverity.Verbose => LogEventLevel.Debug, - LogSeverity.Debug => LogEventLevel.Verbose, + LogSeverity.Verbose => LogEventLevel.Verbose, + LogSeverity.Debug => LogEventLevel.Debug, _ => LogEventLevel.Information }; Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); From daba58cdd4ec699617f35320072ab95e8c04c317 Mon Sep 17 00:00:00 2001 From: Alex Thomson Date: Mon, 18 Apr 2022 18:52:32 +1200 Subject: [PATCH 049/153] Fix SocketGuild not returning the AudioClient (#2248) --- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 49d2cd3bd..8b376b3ed 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1781,7 +1781,7 @@ namespace Discord.WebSocket /// ulong? IGuild.AFKChannelId => AFKChannelId; /// - IAudioClient IGuild.AudioClient => null; + IAudioClient IGuild.AudioClient => AudioClient; /// bool IGuild.Available => true; /// From 42c65bc879c04b528446987bf11c2cd54188a573 Mon Sep 17 00:00:00 2001 From: Denis Voitenko Date: Mon, 18 Apr 2022 09:56:32 +0300 Subject: [PATCH 050/153] Typo in comment (#2242) --- samples/InteractionFramework/Modules/ExampleModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/InteractionFramework/Modules/ExampleModule.cs b/samples/InteractionFramework/Modules/ExampleModule.cs index 1c0a6c8a2..21064bbe3 100644 --- a/samples/InteractionFramework/Modules/ExampleModule.cs +++ b/samples/InteractionFramework/Modules/ExampleModule.cs @@ -14,7 +14,7 @@ namespace InteractionFramework.Modules private InteractionHandler _handler; - // Constructor injection is also a valid way to access the dependecies + // Constructor injection is also a valid way to access the dependencies public ExampleModule(InteractionHandler handler) { _handler = handler; From e1a8ecd723ec3c73706cbca88d606b4bd8d8825d Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Mon, 18 Apr 2022 04:00:58 -0300 Subject: [PATCH 051/153] [Robot] Add missing json error (#2237) * Add 10087, 30047, 30048, 40061 Error codes * Update src/Discord.Net.Core/DiscordErrorCode.cs * Update src/Discord.Net.Core/DiscordErrorCode.cs Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index e9ed63e58..51fd736f6 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -58,6 +58,7 @@ namespace Discord #endregion #region General Actions (20XXX) + UnknownTag = 10087, BotsCannotUse = 20001, OnlyBotsCanUse = 20002, CannotSendExplicitContent = 20009, @@ -98,6 +99,8 @@ namespace Discord #region General Request Errors (40XXX) MaximumNumberOfEditsReached = 30046, + MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047, + MaximumNumberOfTagsInAForumChannelReached = 30048, TokenUnauthorized = 40001, InvalidVerification = 40002, OpeningDMTooFast = 40003, @@ -112,6 +115,7 @@ namespace Discord #region Action Preconditions/Checks (50XXX) InteractionHasAlreadyBeenAcknowledged = 40060, + TagNamesMustBeUnique = 40061, MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, From 18f001e37b5bbdd37b51f933b3b9c89d38c0c400 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Fri, 22 Apr 2022 13:26:54 +0300 Subject: [PATCH 052/153] [DOCS] Group commands example (#2246) * add Group Command Examples to int_framework intro * update subcommad group's name * added some comments t othe example code * fixed naming * added spaces in comments --- docs/guides/int_framework/intro.md | 2 ++ .../samples/intro/groupmodule.cs | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/guides/int_framework/samples/intro/groupmodule.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index abea2a735..f9eca370a 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -282,6 +282,8 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can > Although creating nested module stuctures are allowed, > you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy. +[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] + ## Executing Commands Any of the following socket events can be used to execute commands: diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs new file mode 100644 index 000000000..f0d992aff --- /dev/null +++ b/docs/guides/int_framework/samples/intro/groupmodule.cs @@ -0,0 +1,21 @@ +// You can put commands in groups +[Group("group-name", "Group description")] +public class CommandGroupModule : InteractionModuleBase +{ + // This command will look like + // group-name ping + [SlashCommand("ping", "Get a pong")] + public async Task PongSubcommand() + => await RespondAsync("Pong!"); + + // And even in sub-command groups + [Group("subcommand-group-name", "Subcommand group description")] + public class SubСommandGroupModule : InteractionModuleBase + { + // This command will look like + // group-name subcommand-group-name echo + [SlashCommand("echo", "Echo an input")] + public async Task EchoSubcommand(string input) + => await RespondAsync(input); + } +} \ No newline at end of file From f2d383c955e4f3a93ea4f5b8aaf954c64ea8c195 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 27 Apr 2022 10:59:50 -0300 Subject: [PATCH 053/153] remove extra header from readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 541948f4b..bb8437432 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# Discord.Net

Logo From 275b833205e29244106640af61e9df26d7973d39 Mon Sep 17 00:00:00 2001 From: Alex Thomson Date: Thu, 28 Apr 2022 02:07:35 +1200 Subject: [PATCH 054/153] Fix browser property (#2254) --- src/Discord.Net.WebSocket/DiscordSocketApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 21594fed7..cca2de203 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -274,7 +274,7 @@ namespace Discord.API { ["$device"] = "Discord.Net", ["$os"] = Environment.OSVersion.Platform.ToString(), - [$"browser"] = "Discord.Net" + ["$browser"] = "Discord.Net" }; var msg = new IdentifyParams() { From 26c1a7e80f4e2d73e607fa87708e52724bbe7349 Mon Sep 17 00:00:00 2001 From: Diego-VP20 <69905156+Diego-VP20@users.noreply.github.com> Date: Wed, 27 Apr 2022 16:08:07 +0200 Subject: [PATCH 055/153] docs: Add files to the parameters (#2244) --- .../application-commands/slash-commands/parameters.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guides/int_basics/application-commands/slash-commands/parameters.md b/docs/guides/int_basics/application-commands/slash-commands/parameters.md index 6afd83729..4f3cd2e8c 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/parameters.md +++ b/docs/guides/int_basics/application-commands/slash-commands/parameters.md @@ -15,9 +15,10 @@ Slash commands can have a bunch of parameters, each their own type. Let's first | Integer | A number. | | Boolean | True or False. | | User | A user | -| Channel | A channel, this includes voice text and categories | | Role | A role. | +| Channel | A channel, this includes voice text and categories | | Mentionable | A role or a user. | +| File | A file | Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field: | Name | C# Type | @@ -31,6 +32,7 @@ Each one of the parameter types has its own DNET type in the `SocketSlashCommand | Role | `SocketRole` | | Channel | `SocketChannel` | | Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` | +| File | `IAttachment` | Let's start by making a command that takes in a user and lists their roles. From 4ce1801bdf56b8b7bc0eca7a5c1e4353ab208d64 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:09:30 +0300 Subject: [PATCH 056/153] feature: Passing CustomId matches into contexts (#2136) * add logic for passing the wild card captures into the context * move concrete impl of IRouteSegmentMatch to internal * Apply suggestions from code review Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * fix build errors * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> --- .../Interactions/IRouteMatchContainer.cs | 24 +++++++++++++++++++ .../Interactions/IRouteSegmentMatch.cs | 16 +++++++++++++ .../Interactions/RouteSegmentMatch.cs | 16 +++++++++++++ .../InteractionContext.cs | 14 ++++++++++- .../InteractionService.cs | 19 +++++++++++++++ .../Interactions/RestInteractionContext.cs | 14 ++++++++++- .../Interactions/SocketInteractionContext.cs | 14 ++++++++++- 7 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs create mode 100644 src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs create mode 100644 src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs diff --git a/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs new file mode 100644 index 000000000..f9a3a3183 --- /dev/null +++ b/src/Discord.Net.Core/Interactions/IRouteMatchContainer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Discord +{ + ///

+ /// Represents a container for temporarily storing CustomId wild card matches of a component. + /// + public interface IRouteMatchContainer + { + /// + /// Gets the collection of captured route segments in this container. + /// + /// + /// A collection of captured route segments. + /// + IEnumerable SegmentMatches { get; } + + /// + /// Sets the property of this container. + /// + /// The collection of captured route segments. + void SetSegmentMatches(IEnumerable segmentMatches); + } +} diff --git a/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs new file mode 100644 index 000000000..675bd6754 --- /dev/null +++ b/src/Discord.Net.Core/Interactions/IRouteSegmentMatch.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Represents an object for storing a CustomId wild card match. + /// + public interface IRouteSegmentMatch + { + /// + /// Gets the captured value of this wild card match. + /// + /// + /// The value of this wild card. + /// + string Value { get; } + } +} diff --git a/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs new file mode 100644 index 000000000..f1d80cfea --- /dev/null +++ b/src/Discord.Net.Core/Interactions/RouteSegmentMatch.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + /// + /// Represents an object for storing a CustomId wild card match. + /// + internal record RouteSegmentMatch : IRouteSegmentMatch + { + /// + public string Value { get; } + + public RouteSegmentMatch(string value) + { + Value = value; + } + } +} diff --git a/src/Discord.Net.Interactions/InteractionContext.cs b/src/Discord.Net.Interactions/InteractionContext.cs index 99a8d8736..024ab5ef8 100644 --- a/src/Discord.Net.Interactions/InteractionContext.cs +++ b/src/Discord.Net.Interactions/InteractionContext.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + namespace Discord.Interactions { /// - public class InteractionContext : IInteractionContext + public class InteractionContext : IInteractionContext, IRouteMatchContainer { /// public IDiscordClient Client { get; } @@ -13,6 +16,8 @@ namespace Discord.Interactions public IUser User { get; } /// public IDiscordInteraction Interaction { get; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } /// /// Initializes a new . @@ -30,5 +35,12 @@ namespace Discord.Interactions User = interaction.User; Interaction = interaction; } + + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 01fb8cc9d..8eb5799d6 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -775,6 +775,9 @@ namespace Discord.Interactions await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } + + SetMatchesIfApplicable(context, result); + return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } @@ -819,9 +822,25 @@ namespace Discord.Interactions await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } + + SetMatchesIfApplicable(context, result); + return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } + private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) + where T : class, ICommandInfo + { + if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) + return; + + var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; + for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) + matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + + matchContainer.SetSegmentMatches(matches); + } + internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) => _typeConverterMap.Get(type, services); diff --git a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs index 196c6133b..d407f5103 100644 --- a/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs +++ b/src/Discord.Net.Rest/Interactions/RestInteractionContext.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading.Tasks; namespace Discord.Rest @@ -6,7 +8,7 @@ namespace Discord.Rest /// /// Represents a Rest based context of an . /// - public class RestInteractionContext : IRestInteractionContext + public class RestInteractionContext : IRestInteractionContext, IRouteMatchContainer where TInteraction : RestInteraction { /// @@ -45,6 +47,9 @@ namespace Discord.Rest /// public Func InteractionResponseCallback { get; set; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } + /// /// Initializes a new . /// @@ -71,6 +76,13 @@ namespace Discord.Rest InteractionResponseCallback = interactionResponseCallback; } + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; + // IInterationContext /// IDiscordClient IInteractionContext.Client => Client; diff --git a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs index 4cd9ef264..a2a101839 100644 --- a/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs +++ b/src/Discord.Net.WebSocket/Interactions/SocketInteractionContext.cs @@ -1,11 +1,13 @@ using Discord.WebSocket; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Discord.Interactions { /// /// Represents a Web-Socket based context of an . /// - public class SocketInteractionContext : IInteractionContext + public class SocketInteractionContext : IInteractionContext, IRouteMatchContainer where TInteraction : SocketInteraction { /// @@ -36,6 +38,9 @@ namespace Discord.Interactions /// public TInteraction Interaction { get; } + /// + public IReadOnlyCollection SegmentMatches { get; private set; } + /// /// Initializes a new . /// @@ -50,6 +55,13 @@ namespace Discord.Interactions Interaction = interaction; } + /// + public void SetSegmentMatches(IEnumerable segmentMatches) => SegmentMatches = segmentMatches.ToImmutableArray(); + + //IRouteMatchContainer + /// + IEnumerable IRouteMatchContainer.SegmentMatches => SegmentMatches; + // IInteractionContext /// IDiscordClient IInteractionContext.Client => Client; From d98b3cc495e9230346e38fbd3e7ff4f95c332ef1 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu, 28 Apr 2022 08:47:52 -0300 Subject: [PATCH 057/153] feature: V2 Permissions (#2222) * Initial V2 permissions * add perms-v2 attributes and properties, add deprecation messages * add perms-v2 properties to command info classes * add perms-v2 fields to Rest/SocketApplicationCommand entities and IApplicationCommand * fix json name of DmPermission field Co-authored-by: Cenngo --- .../ApplicationCommandProperties.cs | 10 +++++ .../ContextMenus/MessageCommandBuilder.cs | 36 +++++++++++++++++- .../ContextMenus/UserCommandBuilder.cs | 36 +++++++++++++++++- .../Interactions/IApplicationCommand.cs | 13 +++++++ .../SlashCommands/SlashCommandBuilder.cs | 34 +++++++++++++++++ .../DefaultMemberPermissionAttribute.cs | 25 ++++++++++++ .../Attributes/DefaultPermissionAttribute.cs | 1 + .../Attributes/EnabledInDmAttribute.cs | 25 ++++++++++++ .../Commands/ContextCommandBuilder.cs | 38 +++++++++++++++++++ .../Builders/Commands/SlashCommandBuilder.cs | 38 +++++++++++++++++++ .../Builders/ModuleBuilder.cs | 38 +++++++++++++++++++ .../Builders/ModuleClassBuilder.cs | 30 +++++++++++++++ .../ContextCommands/ContextCommandInfo.cs | 8 ++++ .../Info/Commands/SlashCommandInfo.cs | 8 ++++ .../Info/IApplicationCommandInfo.cs | 13 +++++++ .../Info/ModuleInfo.cs | 28 ++++++++++++++ .../Utilities/ApplicationCommandRestUtil.cs | 21 ++++++++-- .../API/Common/ApplicationCommand.cs | 7 ++++ .../Rest/CreateApplicationCommandParams.cs | 6 +++ .../Interactions/InteractionHelper.cs | 25 ++++++++++-- .../Interactions/RestApplicationCommand.cs | 10 +++++ .../SocketApplicationCommand.cs | 10 +++++ 22 files changed, 451 insertions(+), 9 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 501a0e905..9b3ac8453 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -17,6 +17,16 @@ namespace Discord /// public Optional IsDefaultPermission { get; set; } + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public Optional IsDMEnabled { get; set; } + + /// + /// Gets or sets the default permissions required by a user to execute this application command. + /// + public Optional DefaultMemberPermissions { get; set; } + internal ApplicationCommandProperties() { } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index c7a7cf741..59040dd4e 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -31,6 +31,16 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; /// @@ -44,7 +54,9 @@ namespace Discord var props = new MessageCommandProperties { Name = Name, - IsDefaultPermission = IsDefaultPermission + IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; return props; @@ -73,5 +85,27 @@ namespace Discord IsDefaultPermission = isDefaultPermission; return this; } + + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public MessageCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public MessageCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index bd1078be3..7c82dce55 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -31,6 +31,16 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; /// @@ -42,7 +52,9 @@ namespace Discord var props = new UserCommandProperties { Name = Name, - IsDefaultPermission = IsDefaultPermission + IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; return props; @@ -71,5 +83,27 @@ namespace Discord IsDefaultPermission = isDefaultPermission; return this; } + + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public UserCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public UserCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 72045a52a..58a002649 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -34,6 +34,19 @@ namespace Discord /// bool IsDefaultPermission { get; } + /// + /// Indicates whether the command is available in DMs with the app. + /// + /// + /// Only for globally-scoped commands. + /// + bool IsEnabledInDm { get; } + + /// + /// Set of default required to invoke the command. + /// + GuildPermissions DefaultMemberPermissions { get; } + /// /// Gets a collection of options for this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index ccfb2da0a..ed815ca1a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -81,6 +81,16 @@ namespace Discord ///
public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets or sets whether or not this command can be used in DMs. + /// + public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets the default permission required to use this slash command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } + private string _name; private string _description; private List _options; @@ -96,6 +106,8 @@ namespace Discord Name = Name, Description = Description, IsDefaultPermission = IsDefaultPermission, + IsDMEnabled = IsDMEnabled, + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; if (Options != null && Options.Any()) @@ -145,6 +157,28 @@ namespace Discord return this; } + /// + /// Sets whether or not this command can be used in dms + /// + /// if the command is available in dms, otherwise . + /// The current builder. + public SlashCommandBuilder WithDMPermission(bool permission) + { + IsDMEnabled = permission; + return this; + } + + /// + /// Sets the default member permissions required to use this application command. + /// + /// The permissions required to use this command. + /// The current builder. + public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission? permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + /// /// Adds an option to the current slash command. /// diff --git a/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs new file mode 100644 index 000000000..ec79da1e3 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/DefaultMemberPermissionAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the of an application command or module. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class DefaultMemberPermissionsAttribute : Attribute + { + /// + /// Gets the default permission required to use this command. + /// + public GuildPermission Permissions { get; } + + /// + /// Sets the of an application command or module. + /// + /// The default permission required to use this command. + public DefaultMemberPermissionsAttribute(GuildPermission permissions) + { + Permissions = permissions; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs index ed0a532be..2e03dfac6 100644 --- a/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/DefaultPermissionAttribute.cs @@ -6,6 +6,7 @@ namespace Discord.Interactions /// Set the "Default Permission" property of an Application Command. ///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [Obsolete($"Soon to be deprecated, use Permissions-v2 attributes like {nameof(EnabledInDmAttribute)} and {nameof(DefaultMemberPermissionsAttribute)}")] public class DefaultPermissionAttribute : Attribute { /// diff --git a/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs new file mode 100644 index 000000000..a97f85a25 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/EnabledInDmAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the property of an application command or module. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class EnabledInDmAttribute : Attribute + { + /// + /// Gets whether or not this command can be used in DMs. + /// + public bool IsEnabled { get; } + + /// + /// Sets the property of an application command or module. + /// + /// Whether or not this command can be used in DMs. + public EnabledInDmAttribute(bool isEnabled) + { + IsEnabled = isEnabled; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs index d40547b3c..be0e5eb70 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs @@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders /// /// Gets the default permission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + internal ContextCommandBuilder (ModuleBuilder module) : base(module) { } /// @@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public ContextCommandBuilder SetDefaultPermission (bool defaultPermision) { DefaultPermission = defaultPermision; @@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ContextCommandBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ContextCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + internal override ContextCommandInfo Build (ModuleInfo module, InteractionService commandService) => ContextCommandInfo.Create(this, module, commandService); } diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs index d8e9b0658..cd9bdfc24 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs @@ -17,8 +17,19 @@ namespace Discord.Interactions.Builders /// /// Gets and sets the default permission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + internal SlashCommandBuilder (ModuleBuilder module) : base(module) { } /// @@ -49,6 +60,7 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public SlashCommandBuilder WithDefaultPermission (bool permission) { DefaultPermission = permission; @@ -70,6 +82,32 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + internal override SlashCommandInfo Build (ModuleInfo module, InteractionService commandService) => new SlashCommandInfo(this, module, commandService); } diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index 40c263643..b7f00025f 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -51,8 +51,19 @@ namespace Discord.Interactions.Builders /// /// Gets and sets the default permission of this module. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; set; } = true; + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; set; } = true; + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; set; } = null; + /// /// Gets and sets whether this has a . /// @@ -159,12 +170,39 @@ namespace Discord.Interactions.Builders /// /// The builder instance. /// + [Obsolete($"To be deprecated soon, use {nameof(SetEnabledInDm)} and {nameof(WithDefaultMemberPermissions)} instead.")] public ModuleBuilder WithDefaultPermission (bool permission) { DefaultPermission = permission; return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModuleBuilder SetEnabledInDm(bool isEnabled) + { + IsEnabledInDm = isEnabled; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModuleBuilder WithDefaultMemberPermissions(GuildPermission permissions) + { + DefaultMemberPermissions = permissions; + return this; + } + /// /// Adds attributes to . /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index b2317d1f3..1bbdfcc4a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -85,6 +85,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.AddPreconditions(precondition); break; @@ -169,6 +179,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defaultPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; @@ -211,6 +231,16 @@ namespace Discord.Interactions.Builders builder.DefaultPermission = defaultPermission.IsDefaultPermission; } break; + case EnabledInDmAttribute enabledInDm: + { + builder.IsEnabledInDm = enabledInDm.IsEnabled; + } + break; + case DefaultMemberPermissionsAttribute memberPermission: + { + builder.DefaultMemberPermissions = memberPermission.Permissions; + } + break; case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs index 4c2e7af7d..2d6d748d4 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs @@ -17,6 +17,12 @@ namespace Discord.Interactions /// public bool DefaultPermission { get; } + /// + public bool IsEnabledInDm { get; } + + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// public override IReadOnlyCollection Parameters { get; } @@ -31,6 +37,8 @@ namespace Discord.Interactions { CommandType = builder.CommandType; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); } diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index a123ac183..e428144c7 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -26,6 +26,12 @@ namespace Discord.Interactions /// public bool DefaultPermission { get; } + /// + public bool IsEnabledInDm { get; } + + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// public override IReadOnlyCollection Parameters { get; } @@ -41,6 +47,8 @@ namespace Discord.Interactions { Description = builder.Description; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs index 1e0d532b0..dd1b97899 100644 --- a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace Discord.Interactions { /// @@ -18,6 +20,17 @@ namespace Discord.Interactions /// /// Gets the DefaultPermission of this command. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] bool DefaultPermission { get; } + + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; } + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; } } } diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 321e0bfa9..904d67410 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -41,8 +41,19 @@ namespace Discord.Interactions /// /// Gets the default Permission of this module. /// + [Obsolete($"To be deprecated soon, use {nameof(IsEnabledInDm)} and {nameof(DefaultMemberPermissions)} instead.")] public bool DefaultPermission { get; } + /// + /// Gets whether this command can be used in DMs. + /// + public bool IsEnabledInDm { get; } + + /// + /// Gets the default permissions needed for executing this command. + /// + public GuildPermission? DefaultMemberPermissions { get; } + /// /// Gets the collection of Sub Modules of this module. /// @@ -110,6 +121,8 @@ namespace Discord.Interactions Description = builder.Description; Parent = parent; DefaultPermission = builder.DefaultPermission; + IsEnabledInDm = builder.IsEnabledInDm; + DefaultMemberPermissions = BuildDefaultMemberPermissions(builder); SlashCommands = BuildSlashCommands(builder).ToImmutableArray(); ContextCommands = BuildContextCommands(builder).ToImmutableArray(); ComponentCommands = BuildComponentCommands(builder).ToImmutableArray(); @@ -226,5 +239,20 @@ namespace Discord.Interactions } return true; } + + private static GuildPermission? BuildDefaultMemberPermissions(ModuleBuilder builder) + { + var permissions = builder.DefaultMemberPermissions; + + var parent = builder.Parent; + + while (parent != null) + { + permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0); + parent = parent.Parent; + } + + return permissions; + } } } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index c2052b7c7..60980c065 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -40,7 +40,8 @@ namespace Discord.Interactions { Name = commandInfo.Name, Description = commandInfo.Description, - IsDefaultPermission = commandInfo.DefaultPermission, + IsDMEnabled = commandInfo.IsEnabledInDm, + DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0) }.Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) @@ -64,8 +65,20 @@ namespace Discord.Interactions public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) => commandInfo.CommandType switch { - ApplicationCommandType.Message => new MessageCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission}.Build(), - ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission=commandInfo.DefaultPermission}.Build(), + ApplicationCommandType.Message => new MessageCommandBuilder + { + Name = commandInfo.Name, + IsDefaultPermission = commandInfo.DefaultPermission, + DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0), + IsDMEnabled = commandInfo.IsEnabledInDm + }.Build(), + ApplicationCommandType.User => new UserCommandBuilder + { + Name = commandInfo.Name, + IsDefaultPermission = commandInfo.DefaultPermission, + DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0), + IsDMEnabled = commandInfo.IsEnabledInDm + }.Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") }; #endregion @@ -113,6 +126,8 @@ namespace Discord.Interactions Name = moduleInfo.SlashGroupName, Description = moduleInfo.Description, IsDefaultPermission = moduleInfo.DefaultPermission, + IsDMEnabled = moduleInfo.IsEnabledInDm, + DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions }.Build(); if (options.Count > SlashCommandBuilder.MaxOptionsCount) diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 81598b96e..8b84149dd 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -24,5 +24,12 @@ namespace Discord.API [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } + + // V2 Permissions + [JsonProperty("dm_permission")] + public Optional DmPermission { get; set; } + + [JsonProperty("default_member_permissions")] + public Optional DefaultMemberPermission { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 82f0befcd..7ae8718b6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -19,6 +19,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("dm_permission")] + public Optional DmPermission { get; set; } + + [JsonProperty("default_member_permissions")] + public Optional DefaultMemberPermission { get; set; } + public CreateApplicationCommandParams() { } public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index e345bfa94..74d7953ad 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -100,7 +100,12 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() + }; if (arg is SlashCommandProperties slashProps) @@ -134,7 +139,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) @@ -171,7 +180,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) @@ -285,7 +298,11 @@ namespace Discord.Rest Type = arg.Type, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + + // TODO: better conversion to nullable optionals + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), + DmPermission = arg.IsDMEnabled.ToNullable() }; if (arg is SlashCommandProperties slashProps) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index ea8d5bc42..9e2bab2c2 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -27,6 +27,12 @@ namespace Discord.Rest /// public bool IsDefaultPermission { get; private set; } + /// + public bool IsEnabledInDm { get; private set; } + + /// + public GuildPermissions DefaultMemberPermissions { get; private set; } + /// /// Gets a collection of options for this command. /// @@ -57,6 +63,10 @@ namespace Discord.Rest Options = model.Options.IsSpecified ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); + DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified + ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; } /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 36eba0cd1..40ec17f5b 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -36,6 +36,12 @@ namespace Discord.WebSocket /// public bool IsDefaultPermission { get; private set; } + /// + public bool IsEnabledInDm { get; private set; } + + /// + public GuildPermissions DefaultMemberPermissions { get; private set; } + /// /// Gets a collection of s for this command. /// @@ -86,6 +92,10 @@ namespace Discord.WebSocket Options = model.Options.IsSpecified ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); + DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified + ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; } /// From 2b49322a54d252c80b91756325f54f42ced80016 Mon Sep 17 00:00:00 2001 From: Ge Date: Thu, 28 Apr 2022 19:48:11 +0800 Subject: [PATCH 058/153] docs: Fix TextCommands reference in first-bot.md (#2264) --- docs/guides/getting_started/first-bot.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index e1af20d30..a5b0dbbd4 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -202,7 +202,7 @@ online in Discord. To create commands for your bot, you may choose from a variety of command processors available. Throughout the guides, we will be using -the one that Discord.Net ships with. @Guides.Commands.Intro will +the one that Discord.Net ships with. @Guides.TextCommands.Intro will guide you through how to setup a program that is ready for [CommandService]. From f5dbb95610d7a5cff5f33c2075c316b05e6ae5ed Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu, 28 Apr 2022 14:48:37 +0300 Subject: [PATCH 059/153] docs: Interaction Service Perms-v2 docs (#2263) * add perms v2 docs * add perms v2 docs --- docs/guides/int_framework/intro.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index f9eca370a..c019b1424 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -158,6 +158,14 @@ Interaction service complex parameter constructors are prioritized in the follow 2. Constuctor tagged with `[ComplexParameterCtor]`. 3. Type's only public constuctor. +#### DM Permissions + +You can use the [EnabledInDmAttribute] to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands. + +#### Default Member Permissions + +[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups. + ## User Commands A valid User Command must have the following structure: From 0554ac24429c7574f8bb14f87efea8b4821e5d05 Mon Sep 17 00:00:00 2001 From: Christoph L <47949835+Sir-Photch@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:49:38 +0200 Subject: [PATCH 060/153] fix: Guarding against empty descriptions in `SlashCommandBuilder`/`SlashCommandOptionBuilder` (#2260) * adding null/empty check for option-descriptions * moving check to Preconditions * docs --- .../SlashCommands/SlashCommandBuilder.cs | 27 ++++++------------- src/Discord.Net.Core/Utils/Preconditions.cs | 17 ++++++++++++ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index ed815ca1a..bf74a160c 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -198,21 +198,13 @@ namespace Discord string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { - // Make sure the name matches the requirements from discord - Preconditions.NotNullOrEmpty(name, nameof(name)); - Preconditions.AtLeast(name.Length, 1, nameof(name)); - Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + Preconditions.Options(name, description); // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - // same with description - Preconditions.NotNullOrEmpty(description, nameof(description)); - Preconditions.AtLeast(description.Length, 1, nameof(description)); - Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); - // make sure theres only one option with default set to true if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true) throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); @@ -248,6 +240,7 @@ namespace Discord throw new InvalidOperationException($"Cannot have more than {MaxOptionsCount} options!"); Preconditions.NotNull(option, nameof(option)); + Preconditions.Options(option.Name, option.Description); // this is a double-check when this method is called via AddOption(string name... ) Options.Add(option); return this; @@ -270,6 +263,9 @@ namespace Discord if (Options.Count + options.Length > MaxOptionsCount) throw new ArgumentOutOfRangeException(nameof(options), $"Cannot have more than {MaxOptionsCount} options!"); + foreach (var option in options) + Preconditions.Options(option.Name, option.Description); + Options.AddRange(options); return this; } @@ -413,7 +409,7 @@ namespace Discord MinValue = MinValue, MaxValue = MaxValue }; - } + } /// /// Adds an option to the current slash command. @@ -434,21 +430,13 @@ namespace Discord string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { - // Make sure the name matches the requirements from discord - Preconditions.NotNullOrEmpty(name, nameof(name)); - Preconditions.AtLeast(name.Length, 1, nameof(name)); - Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + Preconditions.Options(name, description); // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - // same with description - Preconditions.NotNullOrEmpty(description, nameof(description)); - Preconditions.AtLeast(description.Length, 1, nameof(description)); - Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); - // make sure theres only one option with default set to true if (isDefault && Options?.Any(x => x.IsDefault == true) == true) throw new ArgumentException("There can only be one command option with default set to true!", nameof(isDefault)); @@ -483,6 +471,7 @@ namespace Discord throw new InvalidOperationException($"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); Preconditions.NotNull(option, nameof(option)); + Preconditions.Options(option.Name, option.Description); // double check again Options.Add(option); return this; diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index ff8eb7c0d..2f24e660d 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -297,5 +297,22 @@ namespace Discord } } #endregion + + #region SlashCommandOptions + + /// or is null. + /// or are either empty or their length exceed limits. + public static void Options(string name, string description) + { + // Make sure the name matches the requirements from discord + NotNullOrEmpty(name, nameof(name)); + NotNullOrEmpty(description, nameof(description)); + AtLeast(name.Length, 1, nameof(name)); + AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + AtLeast(description.Length, 1, nameof(description)); + AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } + + #endregion } } From 9bd088f9b970342e67c613670501d8d0f1dcbfdd Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 28 Apr 2022 17:21:00 +0200 Subject: [PATCH 061/153] [Docs] Adding permission docs for interaction framework (#2265) * Get rid of mediatrsample sln * Add framework perms doc * Append suggestion Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Append suggestion Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- docs/guides/int_framework/permissions.md | 59 +++++++++++++++++++ .../samples/permissions/guild-only.cs | 6 ++ .../samples/permissions/guild-perms.cs | 7 +++ .../samples/permissions/perm-nesting.cs | 16 +++++ .../samples/permissions/perm-stacking.cs | 4 ++ docs/guides/toc.yml | 2 + samples/MediatRSample/MediatRSample.sln | 16 ----- 7 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 docs/guides/int_framework/permissions.md create mode 100644 docs/guides/int_framework/samples/permissions/guild-only.cs create mode 100644 docs/guides/int_framework/samples/permissions/guild-perms.cs create mode 100644 docs/guides/int_framework/samples/permissions/perm-nesting.cs create mode 100644 docs/guides/int_framework/samples/permissions/perm-stacking.cs delete mode 100644 samples/MediatRSample/MediatRSample.sln diff --git a/docs/guides/int_framework/permissions.md b/docs/guides/int_framework/permissions.md new file mode 100644 index 000000000..e35bb162d --- /dev/null +++ b/docs/guides/int_framework/permissions.md @@ -0,0 +1,59 @@ +--- +uid: Guides.IntFw.Perms +title: How to handle permissions. +--- + +# Permissions + +This page covers everything to know about setting up permissions for Slash & context commands. + +Application command (Slash, User & Message) permissions are set up at creation. +When you add your commands to a guild or globally, the permissions will be set up from the attributes you defined. + +Commands that are added will only show up for members that meet the required permissions. +There is no further internal handling, as Discord deals with this on its own. + +> [!WARNING] +> Permissions can only be configured at top level commands. Not in subcommands. + +## Disallowing commands in DM + +Commands can be blocked from being executed in DM if a guild is required to execute them in as followed: + +[!code-csharp[no-DM permission](samples/permissions/guild-only.cs)] + +> [!TIP] +> This attribute only works on global-level commands. Commands that are registered in guilds alone do not have a need for it. + +## Server permissions + +As previously shown, a command like ban can be blocked from being executed inside DMs, +as there are no members to ban inside of a DM. However, for a command like this, +we'll also want to make block it from being used by members that do not have the [permissions]. +To do this, we can use the `DefaultMemberPermissions` attribute: + +[!code-csharp[Server permissions](samples/permissions/guild-perms.cs)] + +### Stacking permissions + +If you want a user to have multiple [permissions] in order to execute a command, you can use the `|` operator, just like with setting up intents: + +[!code-csharp[Permission stacking](samples/permissions/perm-stacking.cs)] + +### Nesting permissions + +Alternatively, permissions can also be nested. +It will look for all uses of `DefaultMemberPermissions` up until the highest level class. +The `EnabledInDm` attribute can be defined at top level as well, +and will be set up for all of the commands & nested modules inside this class. + +[!code-csharp[Permission stacking](samples/permissions/perm-nesting.cs)] + +The amount of nesting you can do is realistically endless. + +> [!NOTE] +> If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. +> As mentioned before, subcommands cannot have seperate permissions from the top level command. + +[permissions]: xref:Discord.GuildPermission + diff --git a/docs/guides/int_framework/samples/permissions/guild-only.cs b/docs/guides/int_framework/samples/permissions/guild-only.cs new file mode 100644 index 000000000..2e907e2d3 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-only.cs @@ -0,0 +1,6 @@ +[EnabledInDm(false)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/guild-perms.cs b/docs/guides/int_framework/samples/permissions/guild-perms.cs new file mode 100644 index 000000000..2853f23e7 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-perms.cs @@ -0,0 +1,7 @@ +[EnabledInDm(false)] +[DefaultMemberPermissions(GuildPermission.BanMembers)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/perm-nesting.cs b/docs/guides/int_framework/samples/permissions/perm-nesting.cs new file mode 100644 index 000000000..8913b1ac1 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-nesting.cs @@ -0,0 +1,16 @@ +[EnabledInDm(true)] +[DefaultMemberPermissions(GuildPermission.ViewChannels)] +public class Module : InteractionModuleBase +{ + [DefaultMemberPermissions(GuildPermission.SendMessages)] + public class NestedModule : InteractionModuleBase + { + // While looking for more permissions, it has found 'ViewChannels' and 'SendMessages'. The result of this lookup will be: + // ViewChannels + SendMessages + ManageMessages. + // If these together are not found for target user, the command will not show up for them. + [DefaultMemberPermissions(GuildPermission.ManageMessages)] + [SlashCommand("ping", "Pong!")] + public async Task Ping() + => await RespondAsync("pong"); + } +} diff --git a/docs/guides/int_framework/samples/permissions/perm-stacking.cs b/docs/guides/int_framework/samples/permissions/perm-stacking.cs new file mode 100644 index 000000000..92cc51477 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-stacking.cs @@ -0,0 +1,4 @@ +[DefaultMemberPermissions(GuildPermission.SendMessages | GuildPermission.ViewChannels)] +[SlashCommand("ping", "Pong!")] +public async Task Ping() + => await RespondAsync("pong"); diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index af0a8e2b4..f122ea6ba 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -57,6 +57,8 @@ topicUid: Guides.IntFw.DI - name: Post-execution Handling topicUid: Guides.IntFw.PostExecution + - name: Permissions + topicUid: Guides.IntFw.Perms - name: Slash Command Basics items: - name: Introduction diff --git a/samples/MediatRSample/MediatRSample.sln b/samples/MediatRSample/MediatRSample.sln deleted file mode 100644 index d0599ae26..000000000 --- a/samples/MediatRSample/MediatRSample.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRSample", "MediatRSample\MediatRSample.csproj", "{CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE066EE5-7ED1-42A0-8DB2-862D44F40EA7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal From 27226f0839581d6e9fd72047c66db5efd225ffce Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Thu, 28 Apr 2022 12:27:29 -0300 Subject: [PATCH 062/153] meta: 3.6.0 --- CHANGELOG.md | 10 +++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4de065c..ac5547568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [3.6.0] - 2022-04-28 +### Added +- #2136 Passing CustomId matches into contexts (4ce1801) +- #2222 V2 Permissions (d98b3cc) + +### Fixed +- #2260 Guarding against empty descriptions in `SlashCommandBuilder`/`SlashCommandOptionBuilder` (0554ac2) +- #2248 Fix SocketGuild not returning the AudioClient (daba58c) +- #2254 Fix browser property (275b833) + ## [3.5.0] - 2022-04-05 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index e50e6eceb..e17f6de98 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.5.0 + 3.6.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 2a4ee2867..585d4dbec 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.5.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.6.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index d79e9a24a..c41f844e1 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.5.0$suffix$ + 3.6.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From a8f607553b1db4209912c3fbfba1c3eb8e5d57df Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Fri, 29 Apr 2022 17:53:14 +0300 Subject: [PATCH 063/153] fix: Permissions v2 Invalid Operation Exception (#2267) * implement fix * implement fix --- .../Entities/Interactions/RestApplicationCommand.cs | 3 +-- .../Interaction/SocketBaseCommand/SocketApplicationCommand.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index 9e2bab2c2..667609ef4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -65,8 +65,7 @@ namespace Discord.Rest : ImmutableArray.Create(); IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); - DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified - ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 40ec17f5b..8f27b65f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -94,8 +94,7 @@ namespace Discord.WebSocket : ImmutableArray.Create(); IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); - DefaultMemberPermissions = model.DefaultMemberPermission.IsSpecified - ? new GuildPermissions((ulong)model.DefaultMemberPermission.Value) : GuildPermissions.None; + DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } /// From 0d74c5cc629e0bb176734a5f2350ecef04de3a94 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Sat, 30 Apr 2022 04:37:22 +0300 Subject: [PATCH 064/153] fix: Implement fix for Custom Id Segments NRE (#2274) --- src/Discord.Net.Interactions/InteractionService.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 8eb5799d6..24302dfc7 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -834,11 +834,16 @@ namespace Discord.Interactions if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer) return; - var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; - for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) - matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); + if (searchResult.RegexCaptureGroups?.Length > 0) + { + var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length]; + for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++) + matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]); - matchContainer.SetSegmentMatches(matches); + matchContainer.SetSegmentMatches(matches); + } + else + matchContainer.SetSegmentMatches(Array.Empty()); } internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null) From 503e720d2141ff3b731b50c09aaae8a69b6223ae Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Sat, 30 Apr 2022 19:02:41 -0300 Subject: [PATCH 065/153] feature: add 50080 Error code (#2272) Co-authored-by: Discord.Net Robot --- src/Discord.Net.Core/DiscordErrorCode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 51fd736f6..a6861c10c 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -152,6 +152,7 @@ namespace Discord InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, CannotDeleteRequiredCommunityChannel = 50074, + CannotEditStickersWithinAMessage = 50080, InvalidSticker = 50081, CannotExecuteOnArchivedThread = 50083, InvalidThreadNotificationSettings = 50084, From f2bb55e8041fb3aac1208d3d666c842019e3f1ae Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sat, 30 Apr 2022 19:02:57 -0300 Subject: [PATCH 066/153] fix: null user on interaction without bot scope (#2271) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index aaef4656a..57d58a8b1 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2331,7 +2331,9 @@ namespace Discord.WebSocket SocketUser user = data.User.IsSpecified ? State.GetOrAddUser(data.User.Value.Id, (_) => SocketGlobalUser.Create(this, State, data.User.Value)) - : guild?.AddOrUpdateUser(data.Member.Value); // null if the bot scope isn't set, so the guild cannot be retrieved. + : guild != null + ? guild.AddOrUpdateUser(data.Member.Value) // null if the bot scope isn't set, so the guild cannot be retrieved. + : State.GetOrAddUser(data.Member.Value.User.Id, (_) => SocketGlobalUser.Create(this, State, data.Member.Value.User)); SocketChannel channel = null; if(data.ChannelId.IsSpecified) From 2f58ddc6a09b22443acbb246ea7baa7ba80708ba Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Sat, 30 Apr 2022 19:06:23 -0300 Subject: [PATCH 067/153] meta: 3.6.1 --- CHANGELOG.md | 13 +++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5547568..023400c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.6.1] - 2022-04-30 +### Added +- #2272 add 50080 Error code (503e720) + +### Fixed +- #2267 Permissions v2 Invalid Operation Exception (a8f6075) +- #2271 null user on interaction without bot scope (f2bb55e) +- #2274 Implement fix for Custom Id Segments NRE (0d74c5c) + +### Misc +- 3.6.0 (27226f0) + + ## [3.6.0] - 2022-04-28 ### Added - #2136 Passing CustomId matches into contexts (4ce1801) diff --git a/Discord.Net.targets b/Discord.Net.targets index e17f6de98..adb0a338c 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.6.0 + 3.6.1 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 585d4dbec..105aa0493 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.6.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.6.1", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index c41f844e1..269657771 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.6.0$suffix$ + 3.6.1$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 6470c64b2d344c2b68d5ea4b6227f6c23298d082 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 1 May 2022 14:30:42 -0300 Subject: [PATCH 068/153] Update FUNDING.yml --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 84ee6e5a1..807381d31 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,3 @@ +github: quinchs open_collective: discordnet +custom: https://paypal.me/quinchs From 5546c705caf1954e72b494e367fd83e636ab9dd8 Mon Sep 17 00:00:00 2001 From: Nikita Petko Date: Mon, 9 May 2022 05:50:26 +0100 Subject: [PATCH 069/153] Remove old url reference in Discord.Net.nuspec (#2286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change organization name within Nuget manifest from RogueException to discord-net. If there’s any other ones I missed, please point it out to me. --- src/Discord.Net/Discord.Net.nuspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 269657771..3985536f4 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -8,10 +8,10 @@ foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp - https://github.com/RogueException/Discord.Net + https://github.com/discord-net/Discord.Net http://opensource.org/licenses/MIT false - https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png @@ -55,4 +55,4 @@ - \ No newline at end of file + From 0ec8938a67f3feda5720ecdda8303595d6d48423 Mon Sep 17 00:00:00 2001 From: moiph Date: Mon, 9 May 2022 18:55:17 -0700 Subject: [PATCH 070/153] feature: Support FailIfNotExists on MessageReference (#2283) Fixes #2282 --- .../Entities/Messages/MessageReference.cs | 15 +++++++++++++-- .../API/Common/MessageReference.cs | 3 +++ .../Entities/Messages/RestMessage.cs | 3 ++- .../Extensions/EntityExtensions.cs | 1 + .../Entities/Messages/SocketMessage.cs | 3 ++- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs index 029910e56..7fdc448ad 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -27,6 +27,12 @@ namespace Discord /// public Optional GuildId { get; internal set; } + /// + /// Gets whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message + /// Defaults to true. + /// + public Optional FailIfNotExists { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -39,16 +45,21 @@ namespace Discord /// /// The ID of the guild that will be referenced. It will be validated if sent. /// - public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null) + /// + /// Whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message. Defaults to true. + /// + public MessageReference(ulong? messageId = null, ulong? channelId = null, ulong? guildId = null, bool? failIfNotExists = null) { MessageId = messageId ?? Optional.Create(); InternalChannelId = channelId ?? Optional.Create(); GuildId = guildId ?? Optional.Create(); + FailIfNotExists = failIfNotExists ?? Optional.Create(); } private string DebuggerDisplay => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + - $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}" + + $"{(FailIfNotExists.IsSpecified ? $", FailIfNotExists: ({FailIfNotExists.Value})" : "")}"; public override string ToString() => DebuggerDisplay; diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs index 6cc7603e0..70ef4e678 100644 --- a/src/Discord.Net.Rest/API/Common/MessageReference.cs +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -12,5 +12,8 @@ namespace Discord.API [JsonProperty("guild_id")] public Optional GuildId { get; set; } + + [JsonProperty("fail_if_not_exists")] + public Optional FailIfNotExists { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index c48a60aac..69e038fd2 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -144,7 +144,8 @@ namespace Discord.Rest { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4062cda3d..f5a88486b 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -87,6 +87,7 @@ namespace Discord.Rest ChannelId = entity.InternalChannelId, GuildId = entity.GuildId, MessageId = entity.MessageId, + FailIfNotExists = entity.FailIfNotExists }; } public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 6668426e1..3cd67beb5 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -182,7 +182,8 @@ namespace Discord.WebSocket { GuildId = model.Reference.Value.GuildId, InternalChannelId = model.Reference.Value.ChannelId, - MessageId = model.Reference.Value.MessageId + MessageId = model.Reference.Value.MessageId, + FailIfNotExists = model.Reference.Value.FailIfNotExists }; } From e13675907301820c5bf3cbb7b63f14be095389af Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Mon, 9 May 2022 22:56:22 -0300 Subject: [PATCH 071/153] feature: Treat warnings as errors and set warning level to 5 (#2270) --- src/Discord.Net.Commands/Discord.Net.Commands.csproj | 2 ++ src/Discord.Net.Commands/Results/MatchResult.cs | 6 +++--- src/Discord.Net.Core/Discord.Net.Core.csproj | 2 ++ src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 1 - .../Entities/Guilds/IGuildScheduledEvent.cs | 2 +- .../Interactions/ApplicationCommandOptionType.cs | 2 +- .../Entities/Interactions/IDiscordInteraction.cs | 2 +- .../MessageComponents/ComponentBuilder.cs | 10 +++++----- .../Entities/Interactions/Modals/ModalBuilder.cs | 8 ++++---- .../Entities/Users/GuildUserProperties.cs | 2 +- src/Discord.Net.Core/Entities/Users/IGuildUser.cs | 6 +++--- src/Discord.Net.Core/Utils/UrlValidation.cs | 2 +- .../Attributes/AutocompleteAttribute.cs | 6 +++--- .../Attributes/Modals/ModalInputAttribute.cs | 2 -- .../Attributes/Modals/ModalTextInputAttribute.cs | 2 +- .../Preconditions/RequireUserPermissionAttribute.cs | 4 ++-- .../Builders/Commands/SlashCommandBuilder.cs | 2 +- .../Modals/Inputs/TextInputComponentBuilder.cs | 2 +- .../Builders/Modals/ModalBuilder.cs | 2 +- .../Builders/ModuleBuilder.cs | 3 ++- .../Builders/Parameters/ParameterBuilder.cs | 2 +- .../Discord.Net.Interactions.csproj | 4 +++- src/Discord.Net.Interactions/InteractionContext.cs | 3 +-- .../InteractionModuleBase.cs | 12 ++++++------ src/Discord.Net.Interactions/InteractionService.cs | 12 ++++++------ .../RestInteractionModuleBase.cs | 4 ++-- .../Results/TypeConverterResult.cs | 2 +- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 2 ++ src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 1 - .../Entities/Interactions/RestInteraction.cs | 1 - src/Discord.Net.Rest/Entities/Roles/RestRole.cs | 2 +- src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs | 2 +- src/Discord.Net.Rest/Net/Queue/RequestQueue.cs | 11 +++-------- .../Discord.Net.WebSocket.csproj | 2 ++ .../Entities/Guilds/SocketGuild.cs | 1 - src/Discord.Net.Webhook/Discord.Net.Webhook.csproj | 2 ++ 36 files changed, 66 insertions(+), 65 deletions(-) diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index fea719016..4fdecd254 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -7,6 +7,8 @@ A Discord.Net extension adding support for bot commands. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs index fb266efa6..5b9bfe72b 100644 --- a/src/Discord.Net.Commands/Results/MatchResult.cs +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord.Commands { @@ -12,7 +12,7 @@ namespace Discord.Commands /// /// Gets on which pipeline stage the command may have matched or failed. /// - public IResult? Pipeline { get; } + public IResult Pipeline { get; } /// public CommandError? Error { get; } @@ -21,7 +21,7 @@ namespace Discord.Commands /// public bool IsSuccess => !Error.HasValue; - private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + private MatchResult(CommandMatch? match, IResult pipeline, CommandError? error, string errorReason) { Match = match; Error = error; diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 783565e04..41d83bbc8 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -7,6 +7,8 @@ The core components for the Discord.Net library. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 4706b629e..775ff9e65 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1173,7 +1173,6 @@ namespace Discord /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index 4b2fa3bee..7219682b7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -89,7 +89,7 @@ namespace Discord /// Gets this events banner image url. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// The cover images url. string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 5bb00797b..4506b66d9 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -56,7 +56,7 @@ namespace Discord Number = 10, /// - /// A . + /// A . /// Attachment = 11 } diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 8f6bef995..9017d310f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -55,7 +55,7 @@ namespace Discord string UserLocale { get; } /// - /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. + /// Gets the preferred locale of the guild this interaction was executed in. if not executed in a guild. /// /// /// Non-community guilds (With no locale setting available) will have en-US as the default value sent by Discord. diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 7becca0e0..9c529f469 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1194,9 +1194,9 @@ namespace Discord /// /// Gets or sets the default value of the text input. /// - /// is less than 0. + /// .Length is less than 0. /// - /// is greater than or . + /// .Length is greater than or . /// public string Value { @@ -1306,18 +1306,18 @@ namespace Discord /// /// Sets the minimum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMinLength(int minLength) { MinLength = minLength; return this; } - + /// /// Sets the maximum length of the current builder. /// - /// The value to set. + /// The value to set. /// The current builder. public TextInputBuilder WithMaxLength(int maxLength) { diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs index 3a3e3cc49..817f69415 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs @@ -64,18 +64,18 @@ namespace Discord /// /// Sets the custom id of the current modal. /// - /// The value to set the custom id to. + /// The value to set the custom id to. /// The current builder. public ModalBuilder WithCustomId(string customId) { CustomId = customId; return this; } - + /// /// Adds a component to the current builder. /// - /// The component to add. + /// The component to add. /// The current builder. public ModalBuilder AddTextInput(TextInputBuilder component) { @@ -213,7 +213,7 @@ namespace Discord /// Adds a to the at the specific row. /// If the row cannot accept the component then it will add it to a row that can. /// - /// The to add. + /// The to add. /// The row to add the text input. /// There are no more rows to add a text input to. /// must be less than . diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 935b956c3..5411f5ebf 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -79,7 +79,7 @@ namespace Discord /// Sets a timestamp how long a user should be timed out for. /// /// - /// or a time in the past to clear a currently existing timeout. + /// or a time in the past to clear a currently existing timeout. /// public Optional TimedOutUntil { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 96de06ed8..9703eafe7 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -104,7 +104,7 @@ namespace Discord /// Gets the date and time that indicates if and for how long a user has been timed out. /// /// - /// or a timestamp in the past if the user is not timed out. + /// or a timestamp in the past if the user is not timed out. /// /// /// A indicating how long the user will be timed out for. @@ -116,7 +116,7 @@ namespace Discord ///
/// /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); @@ -151,7 +151,7 @@ namespace Discord /// If the user does not have a guild avatar, this will be the user's regular avatar. /// /// The format to return. - /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// /// A string representing the URL of the displayed avatar for this user. if the user does not have an avatar in place. /// diff --git a/src/Discord.Net.Core/Utils/UrlValidation.cs b/src/Discord.Net.Core/Utils/UrlValidation.cs index 8e877bd4e..55ae3bdf7 100644 --- a/src/Discord.Net.Core/Utils/UrlValidation.cs +++ b/src/Discord.Net.Core/Utils/UrlValidation.cs @@ -23,7 +23,7 @@ namespace Discord.Utils /// /// Not full URL validation right now. Just Ensures the protocol is either http, https, or discord - /// should be used everything other than url buttons. + /// should be used everything other than url buttons. /// /// The URL to validate before sending to discord. /// A URL must include a protocol (either http, https, or discord). diff --git a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs index e17c9ff14..c8a3428db 100644 --- a/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/AutocompleteAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Set the to . + /// Set the to . /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class AutocompleteAttribute : Attribute @@ -14,7 +14,7 @@ namespace Discord.Interactions public Type AutocompleteHandlerType { get; } /// - /// Set the to and define a to handle + /// Set the to and define a to handle /// Autocomplete interactions targeting the parameter this is applied to. /// /// @@ -29,7 +29,7 @@ namespace Discord.Interactions } /// - /// Set the to without specifying a . + /// Set the to without specifying a . /// public AutocompleteAttribute() { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index d611b574d..e9b877268 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -21,9 +21,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// The label of the input. /// The custom id of the input. - /// Whether the user is required to input a value.> protected ModalInputAttribute(string customId) { CustomId = customId; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 35121cd6b..4439e1d84 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -36,7 +36,7 @@ namespace Discord.Interactions /// /// Create a new . /// - /// + /// The custom id of the text input.> /// The style of the text input. /// The placeholder of the text input. /// The minimum length of the text input's content. diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 77d6e8f25..0f6ecfc66 100644 --- a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -29,7 +29,7 @@ namespace Discord.Interactions /// /// This precondition will always fail if the command is being invoked in a . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// @@ -41,7 +41,7 @@ namespace Discord.Interactions /// /// Requires that the user invoking the command to have a specific . /// - /// + /// /// The that the user must have. Multiple permissions can be /// specified by ORing the permissions together. /// diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs index cd9bdfc24..c21fd5ae8 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs @@ -56,7 +56,7 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 340119ddd..8dd2c4004 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -41,7 +41,7 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs index fc1dbdc0e..c13ff40de 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs @@ -64,7 +64,7 @@ namespace Discord.Interactions.Builders } /// - /// Adds text components to . + /// Adds text components to . /// /// Text Component builder factory. /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index b7f00025f..0eb91ee6a 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -357,7 +357,8 @@ namespace Discord.Interactions.Builders return this; } - + + /// /// Adds a modal command builder to . /// /// factory. diff --git a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs index 78d007d44..fec1a6ce9 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/ParameterBuilder.cs @@ -122,7 +122,7 @@ namespace Discord.Interactions.Builders /// /// Adds preconditions to /// - /// New attributes to be added to . + /// New attributes to be added to . /// /// The builder instance. /// diff --git a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj index c617eff61..a3ac3d508 100644 --- a/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj +++ b/src/Discord.Net.Interactions/Discord.Net.Interactions.csproj @@ -7,8 +7,10 @@ Discord.Interactions Discord.Net.Interactions A Discord.Net extension adding support for Application Commands. + 5 + True - + diff --git a/src/Discord.Net.Interactions/InteractionContext.cs b/src/Discord.Net.Interactions/InteractionContext.cs index 024ab5ef8..b81cc5938 100644 --- a/src/Discord.Net.Interactions/InteractionContext.cs +++ b/src/Discord.Net.Interactions/InteractionContext.cs @@ -24,8 +24,7 @@ namespace Discord.Interactions ///
/// The underlying client. /// The underlying interaction. - /// who executed the command. - /// the command originated from. + /// the command originated from. public InteractionContext(IDiscordClient client, IDiscordInteraction interaction, IMessageChannel channel = null) { Client = client; diff --git a/src/Discord.Net.Interactions/InteractionModuleBase.cs b/src/Discord.Net.Interactions/InteractionModuleBase.cs index 873f4c173..a14779dbb 100644 --- a/src/Discord.Net.Interactions/InteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/InteractionModuleBase.cs @@ -45,7 +45,7 @@ namespace Discord.Interactions protected virtual async Task DeferAsync(bool ephemeral = false, RequestOptions options = null) => await Context.Interaction.DeferAsync(ephemeral, options).ConfigureAwait(false); - /// + /// protected virtual async Task RespondAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.RespondAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -70,7 +70,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.RespondWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task FollowupAsync (string text = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, AllowedMentions allowedMentions = null, RequestOptions options = null, MessageComponent components = null, Embed embed = null) => await Context.Interaction.FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); @@ -95,7 +95,7 @@ namespace Discord.Interactions AllowedMentions allowedMentions = null, MessageComponent components = null, Embed embed = null, RequestOptions options = null) => Context.Interaction.FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options); - /// + /// protected virtual async Task ReplyAsync (string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null) => await Context.Channel.SendMessageAsync(text, false, embed, options, allowedMentions, messageReference, components).ConfigureAwait(false); @@ -118,9 +118,9 @@ namespace Discord.Interactions /// protected virtual async Task RespondWithModalAsync(Modal modal, RequestOptions options = null) => await Context.Interaction.RespondWithModalAsync(modal); - /// - protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where T : class, IModal - => await Context.Interaction.RespondWithModalAsync(customId, options); + /// + protected virtual async Task RespondWithModalAsync(string customId, RequestOptions options = null) where TModal : class, IModal + => await Context.Interaction.RespondWithModalAsync(customId, options); //IInteractionModuleBase diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 24302dfc7..6afa5c086 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -421,7 +421,7 @@ namespace Discord.Interactions ///
/// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// /// The target guild. /// Commands to be registered to Discord. @@ -517,7 +517,7 @@ namespace Discord.Interactions ///
/// /// Commands will be registered as standalone commands, if you want the to take effect, - /// use . Registering a commands without group names might cause the command traversal to fail. + /// use . Registering a commands without group names might cause the command traversal to fail. /// /// Commands to be registered to Discord. /// @@ -965,7 +965,7 @@ namespace Discord.Interactions /// Removes a type reader for the given type. ///
/// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -978,7 +978,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the type . ///
/// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the readers from. @@ -991,7 +991,7 @@ namespace Discord.Interactions /// Removes a generic type reader from the given type. ///
/// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// The type to remove the reader from. @@ -1004,7 +1004,7 @@ namespace Discord.Interactions /// Serialize an object using a into a to be placed in a Component CustomId. /// /// - /// Removing a from the will not dereference the from the loaded module/command instances. + /// Removing a from the will not dereference the from the loaded module/command instances. /// You need to reload the modules for the changes to take effect. /// /// Type of the object to be serialized. diff --git a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs index e83c91fef..b570e6d84 100644 --- a/src/Discord.Net.Interactions/RestInteractionModuleBase.cs +++ b/src/Discord.Net.Interactions/RestInteractionModuleBase.cs @@ -87,12 +87,12 @@ namespace Discord.Interactions await InteractionService._restResponseCallback(Context, payload).ConfigureAwait(false); } - protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) + protected override async Task RespondWithModalAsync(string customId, RequestOptions options = null) { if (Context.Interaction is not RestInteraction restInteraction) throw new InvalidOperationException($"Invalid interaction type. Interaction must be a type of {nameof(RestInteraction)} in order to execute this method"); - var payload = restInteraction.RespondWithModal(customId, options); + var payload = restInteraction.RespondWithModal(customId, options); if (Context is IRestInteractionContext restContext && restContext.InteractionResponseCallback != null) await restContext.InteractionResponseCallback.Invoke(payload).ConfigureAwait(false); diff --git a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs index bd89bf6b7..a9a12ee33 100644 --- a/src/Discord.Net.Interactions/Results/TypeConverterResult.cs +++ b/src/Discord.Net.Interactions/Results/TypeConverterResult.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Interactions { /// - /// Represents a result type for . + /// Represents a result type for . /// public struct TypeConverterResult : IResult { diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 98692998f..bec2396ef 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -7,6 +7,8 @@ A core Discord.Net library containing the REST client and models. net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 92d598466..974ea69ad 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1161,7 +1161,6 @@ namespace Discord.Rest /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 8a8921abe..b8c0f961d 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -333,7 +333,6 @@ namespace Discord.Rest => await FollowupWithFilesAsync(attachments, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// Task IDiscordInteraction.RespondWithFilesAsync(IEnumerable attachments, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); - /// #if NETCOREAPP3_0_OR_GREATER != true /// Task IDiscordInteraction.RespondWithFileAsync(Stream fileStream, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => throw new NotSupportedException("REST-Based interactions don't support files."); diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index a2ad4fd77..df629bec7 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -25,7 +25,7 @@ namespace Discord.Rest public string Name { get; private set; } /// public string Icon { get; private set; } - /// /> + /// public Emoji Emoji { get; private set; } /// public GuildPermissions Permissions { get; private set; } diff --git a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs index cfd64104d..43cd3f902 100644 --- a/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs +++ b/src/Discord.Net.Rest/Net/ED25519/CryptoBytes.cs @@ -243,7 +243,7 @@ namespace Discord.Net.ED25519 /// /// // Decode a base58-encoded string into byte array /// - /// Base58 data string + /// Base58 data string /// Byte array public static byte[] Base58Decode(string input) { diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 75e79eec2..4915a5c39 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -60,14 +60,9 @@ namespace Discord.Net.Queue _clearToken?.Cancel(); _clearToken?.Dispose(); _clearToken = new CancellationTokenSource(); - if (_parentToken != null) - { - _requestCancelTokenSource?.Dispose(); - _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); - _requestCancelToken = _requestCancelTokenSource.Token; - } - else - _requestCancelToken = _clearToken.Token; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; } finally { _tokenLock.Release(); } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 2ce89be5b..a4355bc02 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -8,6 +8,8 @@ net6.0;net5.0;net461;netstandard2.0;netstandard2.1 net6.0;net5.0;netstandard2.0;netstandard2.1 true + 5 + True diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 8b376b3ed..e12f3d1ef 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1291,7 +1291,6 @@ namespace Discord.WebSocket /// in order to use this property. /// /// - /// A collection of speakers for the event. /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index df920b7dc..1e3c3f7f8 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -6,6 +6,8 @@ Discord.Webhook A core Discord.Net library containing the Webhook client and models. net6.0;net5.0;netstandard2.0;netstandard2.1 + 5 + True From 23656e844ee45f4f3a37b3da887f10fb3e6b9a37 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Mon, 9 May 2022 22:57:28 -0300 Subject: [PATCH 072/153] feature: Text-In-Voice (#2269) * Initial implementation * Remove blocking webhooks * add safeguard for tiv * fix tests --- .../Entities/Channels/IVoiceChannel.cs | 2 +- .../Entities/Channels/RestStageChannel.cs | 8 +- .../Entities/Channels/RestTextChannel.cs | 98 +++---- .../Entities/Channels/RestVoiceChannel.cs | 218 ++++++++++++--- .../Entities/Channels/SocketGuildChannel.cs | 2 + .../Entities/Channels/SocketStageChannel.cs | 9 +- .../Entities/Channels/SocketTextChannel.cs | 42 +-- .../Entities/Channels/SocketVoiceChannel.cs | 255 +++++++++++++++--- .../MockedEntities/MockedVoiceChannel.cs | 148 +++------- 9 files changed, 521 insertions(+), 261 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 1d36a41b9..d921a2474 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -6,7 +6,7 @@ namespace Discord /// /// Represents a generic voice channel in a guild. /// - public interface IVoiceChannel : INestedChannel, IAudioChannel, IMentionable + public interface IVoiceChannel : IMessageChannel, INestedChannel, IAudioChannel, IMentionable { /// /// Gets the bit-rate that the clients in this voice channel are requested to use. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs index c01df96fd..b34afd027 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestStageChannel.cs @@ -12,7 +12,11 @@ namespace Discord.Rest public class RestStageChannel : RestVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -37,13 +41,11 @@ namespace Discord.Rest IsLive = isLive; if(isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 76c75ab6e..a73bda334 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -86,25 +86,25 @@ namespace Discord.Rest => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); /// - public Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); /// - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); /// - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -136,7 +136,7 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -146,7 +146,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -156,7 +156,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -166,35 +166,35 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); /// @@ -231,38 +231,6 @@ namespace Discord.Rest public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public virtual Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion - - #region Invites - /// - public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - /// - public virtual async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - - private string DebuggerDisplay => $"{Name} ({Id}, Text)"; - /// /// Creates a thread within this . /// @@ -299,6 +267,38 @@ namespace Discord.Rest var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, invitable, slowmode, options); return RestThreadChannel.Create(Discord, Guild, model); } + + /// + /// Gets the parent (category) channel of this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the category channel + /// representing the parent of this channel; null if none is set. + /// + public virtual Task GetCategoryAsync(RequestOptions options = null) + => ChannelHelper.GetCategoryAsync(this, Discord, options); + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion + + #region Invites + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; #endregion #region ITextChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index bcf03a5bc..31d313a48 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -2,6 +2,7 @@ using Discord.Audio; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -12,21 +13,21 @@ namespace Discord.Rest /// Represents a REST-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel + public class RestVoiceChannel : RestTextChannel, IVoiceChannel, IRestAudioChannel { #region RestVoiceChannel + /// + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. + /// + public virtual bool IsTextInVoice + => Guild.Features.HasTextInVoice; /// public int Bitrate { get; private set; } /// public int? UserLimit { get; private set; } - /// - public ulong? CategoryId { get; private set; } /// public string RTCRegion { get; private set; } - /// - public string Mention => MentionUtils.MentionChannel(Id); - internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) { @@ -41,7 +42,6 @@ namespace Discord.Rest internal override void Update(Model model) { base.Update(model); - CategoryId = model.CategoryId; if(model.Bitrate.IsSpecified) Bitrate = model.Bitrate.Value; @@ -59,41 +59,185 @@ namespace Discord.Rest Update(model); } - /// - /// Gets the parent (category) channel of this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the category channel - /// representing the parent of this channel; null if none is set. - /// - public Task GetCategoryAsync(RequestOptions options = null) - => ChannelHelper.GetCategoryAsync(this, Discord, options); - /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); - #endregion + /// + /// Cannot modify text channel properties of a voice channel. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties of a voice channel"); - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// + /// Cannot create a thread within a voice channel. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Cannot create a thread within a voice channel"); + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + #endregion + #region IAudioChannel /// /// Connecting to a REST-based channel is not supported. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 79f02fe1c..6d9e759b4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -222,6 +222,8 @@ namespace Discord.WebSocket #region IChannel /// + string IChannel.Name => Name; + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 91bca5054..56cd92185 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -15,7 +15,11 @@ namespace Discord.WebSocket public class SocketStageChannel : SocketVoiceChannel, IStageChannel { /// - public string Topic { get; private set; } + /// + /// This field is always false for stage channels. + /// + public override bool IsTextInVoice + => false; /// public StagePrivacyLevel? PrivacyLevel { get; private set; } @@ -49,19 +53,16 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } - internal void Update(StageInstance model, bool isLive = false) { IsLive = isLive; if (isLive) { - Topic = model.Topic; PrivacyLevel = model.PrivacyLevel; IsDiscoverableDisabled = model.DiscoverableDisabled; } else { - Topic = null; PrivacyLevel = null; IsDiscoverableDisabled = null; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e4a299edc..e8454ecf8 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -128,7 +128,7 @@ namespace Discord.WebSocket #region Messages /// - public SocketMessage GetCachedMessage(ulong id) + public virtual SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); /// /// Gets a message from this message channel. @@ -143,7 +143,7 @@ namespace Discord.WebSocket /// A task that represents an asynchronous get operation for retrieving the message. The task result contains /// the retrieved message; null if no message is found with the specified identifier. /// - public async Task GetMessageAsync(ulong id, RequestOptions options = null) + public virtual async Task GetMessageAsync(ulong id, RequestOptions options = null) { IMessage msg = _messages?.Get(id); if (msg == null) @@ -163,7 +163,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -179,7 +179,7 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); /// /// Gets a collection of messages in this channel. @@ -195,25 +195,25 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + public virtual IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); /// - public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); /// - public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); /// - public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public virtual IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); /// - public Task> GetPinnedMessagesAsync(RequestOptions options = null) + public virtual Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, + public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, @@ -221,7 +221,7 @@ namespace Discord.WebSocket /// /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -230,7 +230,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -239,7 +239,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -248,7 +248,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -256,28 +256,28 @@ namespace Discord.WebSocket messageReference, components, stickers, options, embeds, flags); /// - public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); /// - public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + public virtual Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); /// - public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + public virtual async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + public virtual Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); /// - public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + public virtual Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); /// - public Task TriggerTypingAsync(RequestOptions options = null) + public virtual Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); /// - public IDisposable EnterTypingState(RequestOptions options = null) + public virtual IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); internal void AddMessage(SocketMessage msg) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 00003d4ed..5fc99c3f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -14,33 +15,21 @@ namespace Discord.WebSocket /// Represents a WebSocket-based voice channel in a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel + public class SocketVoiceChannel : SocketTextChannel, IVoiceChannel, ISocketAudioChannel { #region SocketVoiceChannel - /// - public int Bitrate { get; private set; } - /// - public int? UserLimit { get; private set; } - /// - public string RTCRegion { get; private set; } - - /// - public ulong? CategoryId { get; private set; } /// - /// Gets the parent (category) channel of this channel. + /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. /// - /// - /// A category channel representing the parent of this channel; null if none is set. - /// - public ICategoryChannel Category - => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + public virtual bool IsTextInVoice + => Guild.Features.HasTextInVoice; /// - public string Mention => MentionUtils.MentionChannel(Id); - + public int Bitrate { get; private set; } + /// + public int? UserLimit { get; private set; } /// - public Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + public string RTCRegion { get; private set; } /// /// Gets a collection of users that are currently connected to this voice channel. @@ -48,7 +37,7 @@ namespace Discord.WebSocket /// /// A read-only collection of users that are currently connected to this voice channel. /// - public override IReadOnlyCollection Users + public IReadOnlyCollection ConnectedUsers => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); internal SocketVoiceChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) @@ -65,7 +54,6 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - CategoryId = model.CategoryId; Bitrate = model.Bitrate.Value; UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; RTCRegion = model.RTCRegion.GetValueOrDefault(null); @@ -99,28 +87,215 @@ namespace Discord.WebSocket return user; return null; } -#endregion - #region Invites - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); - /// - public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); - /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + /// Cannot create threads in voice channels. + public override Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) + => throw new InvalidOperationException("Voice channels cannot contain threads."); + + /// Cannot modify text channel properties for voice channels. + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new InvalidOperationException("Cannot modify text channel properties for voice channels."); + + #endregion + + #region TextOverrides + + /// This function is only supported in Text-In-Voice channels. + public override Task GetMessageAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessageAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(message, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessageAsync(messageId, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messages, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.DeleteMessagesAsync(messageIds, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IDisposable EnterTypingState(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.EnterTypingState(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override SocketMessage GetCachedMessage(ulong id) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessage(id); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessage, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = 100) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetCachedMessages(fromMessageId, dir, limit); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessage, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetMessagesAsync(fromMessageId, dir, limit, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetPinnedMessagesAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetPinnedMessagesAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhookAsync(id, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task> GetWebhooksAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.GetWebhooksAsync(options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.CreateWebhookAsync(name, avatar, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.ModifyMessageAsync(messageId, func, options); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags); + } + + /// This function is only supported in Text-In-Voice channels. + public override Task TriggerTypingAsync(RequestOptions options = null) + { + if (!IsTextInVoice) + throw new NotSupportedException("This function is only supported in Text-In-Voice channels"); + return base.TriggerTypingAsync(options); + } + + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; - #endregion #region IGuildChannel /// diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 533b1b1b5..fdbdeda5e 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; using Discord.Audio; @@ -12,8 +13,6 @@ namespace Discord public int? UserLimit => throw new NotImplementedException(); - public string Mention => throw new NotImplementedException(); - public ulong? CategoryId => throw new NotImplementedException(); public int Position => throw new NotImplementedException(); @@ -24,116 +23,53 @@ namespace Discord public IReadOnlyCollection PermissionOverwrites => throw new NotImplementedException(); + public string RTCRegion => throw new NotImplementedException(); + public string Name => throw new NotImplementedException(); public DateTimeOffset CreatedAt => throw new NotImplementedException(); - public ulong Id => throw new NotImplementedException(); - - public string RTCRegion => throw new NotImplementedException(); - public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotImplementedException(); - } + public ulong Id => throw new NotImplementedException(); - public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) - { - throw new NotImplementedException(); - } + public string Mention => throw new NotImplementedException(); - public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - { - throw new NotImplementedException(); - } - public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); + public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException(); + public Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); - public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => throw new NotImplementedException(); - - public Task DeleteAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task DisconnectAsync() - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options) - { - throw new NotImplementedException(); - } - - public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task> GetInvitesAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IRole role) - { - throw new NotImplementedException(); - } - - public OverwritePermissions? GetPermissionOverwrite(IUser user) - { - throw new NotImplementedException(); - } - - public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task ModifyAsync(Action func, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - { - throw new NotImplementedException(); - } - - public Task SyncPermissionsAsync(RequestOptions options = null) - { - throw new NotImplementedException(); - } - - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } - - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { - throw new NotImplementedException(); - } + public Task CreateInviteToStreamAsync(IUser user, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => throw new NotImplementedException(); + public Task DisconnectAsync() => throw new NotImplementedException(); + public IDisposable EnterTypingState(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetInvitesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = 100, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IRole role) => throw new NotImplementedException(); + public OverwritePermissions? GetPermissionOverwrite(IUser user) => throw new NotImplementedException(); + public Task> GetPinnedMessagesAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) => throw new NotImplementedException(); + public Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task TriggerTypingAsync(RequestOptions options = null) => throw new NotImplementedException(); + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotImplementedException(); + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotImplementedException(); } } From 20ffa645257315c1b273f82222b380c5ff79c560 Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 13 May 2022 13:03:45 -0300 Subject: [PATCH 073/153] fix: Possible NRE in Sanitize (#2290) --- src/Discord.Net.Core/Format.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index dc2a06540..d9ad43f0d 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -37,8 +37,9 @@ namespace Discord /// Sanitizes the string, safely escaping any Markdown sequences. public static string Sanitize(string text) { - foreach (string unsafeChar in SensitiveCharacters) - text = text.Replace(unsafeChar, $"\\{unsafeChar}"); + if (text != null) + foreach (string unsafeChar in SensitiveCharacters) + text = text.Replace(unsafeChar, $"\\{unsafeChar}"); return text; } From b0a3b65bc05e220dbb003b28caaaccd3bce0633e Mon Sep 17 00:00:00 2001 From: openmilk <33862452+openmilk@users.noreply.github.com> Date: Sat, 14 May 2022 08:28:46 +1000 Subject: [PATCH 074/153] feature: Webhook support for threads (#2291) * Added thread support to webhooks Added thread support to delete/send messages for webhooks * Revert "Added thread support to webhooks" This reverts commit c45ef389c5df6a924b6ea5d46d5507386904f965. * read added threads as im a dummy * fixed formating * Fixed modify EmbedMessage --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 52 +++++++++++++------ .../DiscordWebhookClient.cs | 28 +++++----- .../WebhookClientHelper.cs | 33 ++++++------ 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3b829ee17..55e9e13dc 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -173,10 +173,12 @@ namespace Discord.API private async Task LogoutInternalAsync() { //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too. - if (LoginState == LoginState.LoggedOut) return; + if (LoginState == LoginState.LoggedOut) + return; LoginState = LoginState.LoggingOut; - try { _loginCancelToken?.Cancel(false); } + try + { _loginCancelToken?.Cancel(false); } catch { } await DisconnectInternalAsync(null).ConfigureAwait(false); @@ -398,7 +400,7 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); @@ -414,9 +416,9 @@ namespace Discord.API Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - if(args.Name.IsSpecified) + if (args.Name.IsSpecified) Preconditions.AtMost(args.Name.Value.Length, 100, nameof(args.Name)); - if(args.Topic.IsSpecified) + if (args.Topic.IsSpecified) Preconditions.AtMost(args.Topic.Value.Length, 1024, nameof(args.Name)); Preconditions.AtLeast(args.SlowModeInterval, 0, nameof(args.SlowModeInterval)); @@ -798,9 +800,11 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -816,12 +820,12 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null) + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -837,11 +841,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// This operation may only be called with a token. - public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null) + public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -852,7 +856,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", ids, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . @@ -873,7 +877,7 @@ namespace Discord.API /// Message content is too long, length must be less or equal to . /// This operation may only be called with a token. - public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null, ulong? threadId = null) { if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); @@ -893,7 +897,7 @@ namespace Discord.API } var ids = new BucketIds(webhookId: webhookId); - return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -1380,7 +1384,7 @@ namespace Discord.API if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.File.IsSpecified) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if(args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -1400,7 +1404,7 @@ namespace Discord.API throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - + var ids = new BucketIds(); return await SendMultipartAsync("POST", () => $"webhooks/{CurrentApplicationId}/{token}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } @@ -1729,8 +1733,10 @@ namespace Discord.API if (args.TargetType.IsSpecified) { Preconditions.NotEqual((int)args.TargetType.Value, (int)TargetUserType.Undefined, nameof(args.TargetType)); - if (args.TargetType.Value == TargetUserType.Stream) Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); - if (args.TargetType.Value == TargetUserType.EmbeddedApplication) Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); + if (args.TargetType.Value == TargetUserType.Stream) + Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); + if (args.TargetType.Value == TargetUserType.EmbeddedApplication) + Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); } options = RequestOptions.CreateOrClone(options); @@ -2414,6 +2420,18 @@ namespace Discord.API return (expr as MemberExpression).Member.Name; } + + private static string WebhookQuery(bool wait = false, ulong? threadId = null) + { + List querys = new List() { }; + if (wait) + querys.Add("wait=true"); + if (threadId.HasValue) + querys.Add($"thread_id={threadId}"); + + return $"{string.Join("&", querys)}"; + } + #endregion } } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 405100f89..556338956 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -88,8 +88,8 @@ namespace Discord.Webhook /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags, threadId); /// /// Modifies a message posted using this webhook. @@ -103,8 +103,8 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous modification operation. /// - public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) - => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options); + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null, ulong? threadId = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options, threadId); /// /// Deletes a message posted using this webhook. @@ -117,43 +117,43 @@ namespace Discord.Webhook /// /// A task that represents the asynchronous deletion operation. /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) - => WebhookClientHelper.DeleteMessageAsync(this, messageId, options); + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null, ulong ? threadId = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, options, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components, flags); + allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None) + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components, flags); + avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options, flags); + avatarUrl, allowedMentions, components, options, flags, threadId); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, - MessageFlags flags = MessageFlags.None) + MessageFlags flags = MessageFlags.None, ulong? threadId = null) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options, flags); + allowedMentions, components, options, flags, threadId); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 0a974a9d9..8ad74e7e7 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,8 +21,8 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, - AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags, ulong? threadId = null) { var args = new CreateWebhookMessageParams { @@ -44,12 +44,13 @@ namespace Discord.Webhook if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); - - var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); + + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options, threadId: threadId).ConfigureAwait(false); return model.Id; } + public static async Task ModifyMessageAsync(DiscordWebhookClient client, ulong messageId, - Action func, RequestOptions options) + Action func, RequestOptions options, ulong? threadId) { var args = new WebhookMessageProperties(); func(args); @@ -94,35 +95,35 @@ namespace Discord.Webhook Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, }; - await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options) + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) .ConfigureAwait(false); } - public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options) + public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options, ulong? threadId) { - await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); + await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options, threadId).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, - bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None, ulong? threadId = null) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId).ConfigureAwait(false); } public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, - MessageComponent components, MessageFlags flags) - => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, MessageFlags flags, ulong? threadId) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, - MessageComponent components, RequestOptions options, MessageFlags flags) - => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); + MessageComponent components, RequestOptions options, MessageFlags flags, ulong? threadId) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags, threadId); public static async Task SendFilesAsync(DiscordWebhookClient client, IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, - MessageFlags flags) + MessageFlags flags, ulong? threadId) { embeds ??= Array.Empty(); @@ -164,7 +165,7 @@ namespace Discord.Webhook MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Flags = flags }; - var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); + var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options, threadId).ConfigureAwait(false); return msg.Id; } From b333de223792ef9bcc527376b83bbbfc666963f3 Mon Sep 17 00:00:00 2001 From: Nhea Date: Sat, 14 May 2022 03:59:38 +0300 Subject: [PATCH 075/153] feature: add UpdateAsync to SocketModal (#2289) --- .../Interaction/Modals/SocketModal.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index cfbd3096d..647544b48 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -174,6 +174,91 @@ namespace Discord.WebSocket HasResponded = true; } + public async Task UpdateAsync(Action func, RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (!InteractionHelper.CanSendResponse(this)) + throw new TimeoutException($"Cannot respond to an interaction after {InteractionHelper.ResponseTimeLimit} seconds!"); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions), "A max of 100 user Ids are allowed."); + } + + var embed = args.Embed; + var embeds = args.Embeds; + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : false; + bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0; + + if (!hasText && !hasEmbeds) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; + + if (embed.IsSpecified && embed.Value != null) + { + apiEmbeds.Add(embed.Value.ToModel()); + } + + if (embeds.IsSpecified && embeds.Value != null) + { + apiEmbeds.AddRange(embeds.Value.Select(x => x.ToModel())); + } + + Preconditions.AtMost(apiEmbeds?.Count ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (args.AllowedMentions.IsSpecified && args.AllowedMentions.Value != null && args.AllowedMentions.Value.AllowedTypes.HasValue) + { + var allowedMentions = args.AllowedMentions.Value; + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) + && allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(args.AllowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) + && allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(args.AllowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + lock (_lock) + { + if (HasResponded) + { + throw new InvalidOperationException("Cannot respond, update, or defer twice to the same interaction"); + } + } + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + HasResponded = true; + } + /// public override async Task FollowupAsync( string text = null, From 6fbd3968326116131519b66dc53d584dfb063b75 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 18 May 2022 10:45:18 +0300 Subject: [PATCH 076/153] Add Nullable ComponentTypeConverter and TypeReader (#2307) * add nullable ComponentTypeConverter and TypeReader * add converter and reader to interactionservice --- .../InteractionService.cs | 6 +++-- .../NullableComponentConverter.cs | 23 +++++++++++++++++++ .../TypeReaders/NullableReader.cs | 23 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs create mode 100644 src/Discord.Net.Interactions/TypeReaders/NullableReader.cs diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 6afa5c086..f57c75a31 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -223,7 +223,8 @@ namespace Discord.Interactions new ConcurrentDictionary { [typeof(Array)] = typeof(DefaultArrayComponentConverter<>), - [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>) + [typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>), + [typeof(Nullable<>)] = typeof(NullableComponentConverter<>) }); _typeReaderMap = new TypeMap(this, new ConcurrentDictionary(), @@ -234,7 +235,8 @@ namespace Discord.Interactions [typeof(IUser)] = typeof(DefaultUserReader<>), [typeof(IMessage)] = typeof(DefaultMessageReader<>), [typeof(IConvertible)] = typeof(DefaultValueReader<>), - [typeof(Enum)] = typeof(EnumReader<>) + [typeof(Enum)] = typeof(EnumReader<>), + [typeof(Nullable<>)] = typeof(NullableReader<>) }); } diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs new file mode 100644 index 000000000..ba6568ad1 --- /dev/null +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/NullableComponentConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableComponentConverter : ComponentTypeConverter + { + private readonly ComponentTypeConverter _typeConverter; + + public NullableComponentConverter(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeConverter = interactionService.GetComponentTypeConverter(type, services); + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + => string.IsNullOrEmpty(option.Value) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeConverter.ReadAsync(context, option, services); + } +} diff --git a/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs new file mode 100644 index 000000000..ed88dc64a --- /dev/null +++ b/src/Discord.Net.Interactions/TypeReaders/NullableReader.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + internal class NullableReader : TypeReader + { + private readonly TypeReader _typeReader; + + public NullableReader(InteractionService interactionService, IServiceProvider services) + { + var type = Nullable.GetUnderlyingType(typeof(T)); + + if (type is null) + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + + _typeReader = interactionService.GetTypeReader(type, services); + } + + public override Task ReadAsync(IInteractionContext context, string option, IServiceProvider services) + => string.IsNullOrEmpty(option) ? Task.FromResult(TypeConverterResult.FromSuccess(null)) : _typeReader.ReadAsync(context, option, services); + } +} From 94a37156f354ad6ffae385001bc7c333bfcb121a Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:47:23 +0200 Subject: [PATCH 077/153] Resolve NRE at get audit for Deleted User (#2304) --- .../Entities/AuditLogs/DataTypes/BanAuditLogData.cs | 5 ++++- .../Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs | 5 ++++- .../AuditLogs/DataTypes/InviteCreateAuditLogData.cs | 5 ++++- .../AuditLogs/DataTypes/InviteDeleteAuditLogData.cs | 5 ++++- .../Entities/AuditLogs/DataTypes/KickAuditLogData.cs | 7 +++++-- .../Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs | 2 +- .../AuditLogs/DataTypes/MemberUpdateAuditLogData.cs | 5 ++++- .../AuditLogs/DataTypes/MessageDeleteAuditLogData.cs | 6 +++++- .../Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs | 5 ++++- .../AuditLogs/DataTypes/MessageUnpinAuditLogData.cs | 5 ++++- .../Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs | 4 ++-- 11 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index fc807cac0..7246ac197 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BanAuditLogData(RestUser.Create(discord, userInfo)); + return new BanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was banned. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the banned user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs index 0d12e4609..288cb9d0a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -18,12 +18,15 @@ namespace Discord.Rest internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + return new BotAddAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the bot that was added. /// + /// + /// Will be if the bot is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the bot. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index b177b2435..3560b9a27 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 9d0aed12b..2dc2f22f6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -45,7 +45,7 @@ namespace Discord.Rest { var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - inviter = RestUser.Create(discord, inviterInfo); + inviter = (inviterInfo != null) ? RestUser.Create(discord, inviterInfo) : null; } return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); @@ -76,6 +76,9 @@ namespace Discord.Rest /// /// Gets the user that created this invite if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user that created this invite or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs index dceb73d0a..b533f0268 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,12 +18,15 @@ namespace Discord.Rest internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new KickAuditLogData(RestUser.Create(discord, userInfo)); + return new KickAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// /// Gets the user that was kicked. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the kicked user. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs index 763c90c68..276604d03 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -27,7 +27,7 @@ namespace Discord.Rest .ToList(); var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, userInfo); + RestUser user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user); } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs index f22b83e4c..f3437e621 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -33,7 +33,7 @@ namespace Discord.Rest newMute = muteModel?.NewValue?.ToObject(discord.ApiClient.Serializer); var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - var user = RestUser.Create(discord, targetInfo); + RestUser user = (targetInfo != null) ? RestUser.Create(discord, targetInfo) : null; var before = new MemberInfo(oldNick, oldDeaf, oldMute); var after = new MemberInfo(newNick, newDeaf, newMute); @@ -44,6 +44,9 @@ namespace Discord.Rest /// /// Gets the user that the changes were performed on. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user who the changes were performed on. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index 66b3f7d83..746fc2ea6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -2,6 +2,7 @@ using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; +using System; namespace Discord.Rest { @@ -20,7 +21,7 @@ namespace Discord.Rest internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, userInfo != null ? RestUser.Create(discord, userInfo) : null); } /// @@ -41,6 +42,9 @@ namespace Discord.Rest /// /// Gets the user of the messages that were deleted. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the deleted messages. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs index be66ac846..c33fd5f44 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was pinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the pinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs index b4fa389cc..f6fd31771 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -23,7 +23,7 @@ namespace Discord.Rest if (entry.TargetId.HasValue) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - user = RestUser.Create(discord, userInfo); + user = (userInfo != null) ? RestUser.Create(discord, userInfo) : null; } return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); @@ -46,6 +46,9 @@ namespace Discord.Rest /// /// Gets the user of the message that was unpinned if available. /// + /// + /// Will be if the user is a 'Deleted User#....' because Discord does send user data for deleted users. + /// /// /// A user object representing the user that created the unpinned message or . /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs index bc7e7fd4f..f12d9a1af 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -18,7 +18,7 @@ namespace Discord.Rest internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new UnbanAuditLogData(RestUser.Create(discord, userInfo)); + return new UnbanAuditLogData((userInfo != null) ? RestUser.Create(discord, userInfo) : null); } /// From 442fea13405a865ae0c04b9195dbcac61132e743 Mon Sep 17 00:00:00 2001 From: Raiden Shogun Date: Wed, 18 May 2022 09:47:55 +0200 Subject: [PATCH 078/153] Added `IAttachment` to docs#2302) --- docs/guides/int_framework/intro.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index c019b1424..54e9086a1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -86,6 +86,7 @@ By default, your methods can feature the following parameter types: - Implementations of [IChannel] - Implementations of [IRole] - Implementations of [IMentionable] +- Implementations of [IAttachment] - `string` - `float`, `double`, `decimal` - `bool` From e35fbedc0a3faf5f4aeea80e2b23acf782dcb0c1 Mon Sep 17 00:00:00 2001 From: Jeroen Heijster Date: Wed, 18 May 2022 09:48:10 +0200 Subject: [PATCH 079/153] Fixed typos. (#2300) --- .github/ISSUE_TEMPLATE/bugreport.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index e2c154130..29759facf 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -38,7 +38,7 @@ body: id: description attributes: label: Description - description: A brief explination of the bug. + description: A brief explanation of the bug. placeholder: When I start a DiscordSocketClient without stopping it, the gateway thread gets blocked. validations: required: true @@ -62,7 +62,7 @@ body: id: logs attributes: label: Logs - description: Add applicable logs and/or a stacktrace here. + description: Add applicable logs and/or a stack trace here. validations: required: true - type: textarea From 725d2557dd349544318775ae3012525428bacbd4 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:48:34 +0200 Subject: [PATCH 080/153] fix: close-stage bucketId being null (#2299) --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 55e9e13dc..dcb13d9e3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -673,9 +673,11 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); + var bucket = new BucketIds(channelId: channelId); + try { - await SendAsync("DELETE", $"stage-instances/{channelId}", options: options).ConfigureAwait(false); + await SendAsync("DELETE", () => $"stage-instances/{channelId}", bucket, options: options).ConfigureAwait(false); } catch (HttpException httpEx) when (httpEx.HttpCode == HttpStatusCode.NotFound) { } } From 6d21e42ddf3a23a4d25b8e882cfd5579efa0cc34 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:49:21 +0200 Subject: [PATCH 081/153] Replace Project- with PackageReference on samples. (#2297) * Init * Resolve errors --- samples/BasicBot/_BasicBot.csproj | 6 +++--- .../InteractionFramework/_InteractionFramework.csproj | 10 ++-------- samples/ShardedClient/_ShardedClient.csproj | 9 ++------- .../TextCommandFramework/_TextCommandFramework.csproj | 9 +++------ samples/WebhookClient/_WebhookClient.csproj | 4 ++-- src/Discord.Net.Examples/Discord.Net.Examples.csproj | 2 +- 6 files changed, 13 insertions(+), 27 deletions(-) diff --git a/samples/BasicBot/_BasicBot.csproj b/samples/BasicBot/_BasicBot.csproj index 6e1a6365f..e6245d340 100644 --- a/samples/BasicBot/_BasicBot.csproj +++ b/samples/BasicBot/_BasicBot.csproj @@ -1,12 +1,12 @@ - + Exe - net5.0 + net6.0 - + diff --git a/samples/InteractionFramework/_InteractionFramework.csproj b/samples/InteractionFramework/_InteractionFramework.csproj index f11c2bd3d..8892a65b7 100644 --- a/samples/InteractionFramework/_InteractionFramework.csproj +++ b/samples/InteractionFramework/_InteractionFramework.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 InteractionFramework @@ -13,13 +13,7 @@ - - - - - - - + diff --git a/samples/ShardedClient/_ShardedClient.csproj b/samples/ShardedClient/_ShardedClient.csproj index 69576ea27..68a43c7cd 100644 --- a/samples/ShardedClient/_ShardedClient.csproj +++ b/samples/ShardedClient/_ShardedClient.csproj @@ -2,18 +2,13 @@ Exe - net5.0 + net6.0 ShardedClient - - - - - - + diff --git a/samples/TextCommandFramework/_TextCommandFramework.csproj b/samples/TextCommandFramework/_TextCommandFramework.csproj index ee64205f5..6e00625e8 100644 --- a/samples/TextCommandFramework/_TextCommandFramework.csproj +++ b/samples/TextCommandFramework/_TextCommandFramework.csproj @@ -2,17 +2,14 @@ Exe - net5.0 + net6.0 TextCommandFramework - - - - - + + diff --git a/samples/WebhookClient/_WebhookClient.csproj b/samples/WebhookClient/_WebhookClient.csproj index 91131894d..515fcf3a4 100644 --- a/samples/WebhookClient/_WebhookClient.csproj +++ b/samples/WebhookClient/_WebhookClient.csproj @@ -2,12 +2,12 @@ Exe - net5.0 + net6.0 WebHookClient - + diff --git a/src/Discord.Net.Examples/Discord.Net.Examples.csproj b/src/Discord.Net.Examples/Discord.Net.Examples.csproj index b4a336f9f..1bdca7992 100644 --- a/src/Discord.Net.Examples/Discord.Net.Examples.csproj +++ b/src/Discord.Net.Examples/Discord.Net.Examples.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 From 13ccc7c9972c2b55983de9d75cf35b29db5fd30b Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Wed, 18 May 2022 10:50:55 +0300 Subject: [PATCH 082/153] feature: Add `.With` methods to ActionRowBuilder (#2296) * Added `.With` methods to `ActionRowBuilder` - Added `.WithButton` & `.WithSelectMenu` methods to `ActionRowBuilder` - fixed a typo * removed `` from methods which don't directly throw an exception * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs * Update src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- .../MessageComponents/ComponentBuilder.cs | 100 +++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 9c529f469..37342b039 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -195,7 +195,7 @@ namespace Discord /// /// The button to add. /// The row to add the button. - /// There is no more row to add a menu. + /// There is no more row to add a button. /// must be less than . /// The current builder. public ComponentBuilder WithButton(ButtonBuilder button, int row = 0) @@ -348,6 +348,100 @@ namespace Discord return this; } + /// + /// Adds a to the . + /// + /// The custom id of the menu. + /// The options of the menu. + /// The placeholder of the menu. + /// The min values of the placeholder. + /// The max values of the placeholder. + /// Whether or not the menu is disabled. + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) + { + return WithSelectMenu(new SelectMenuBuilder() + .WithCustomId(customId) + .WithOptions(options) + .WithPlaceholder(placeholder) + .WithMaxValues(maxValues) + .WithMinValues(minValues) + .WithDisabled(disabled)); + } + + /// + /// Adds a to the . + /// + /// The menu to add. + /// A Select Menu cannot exist in a pre-occupied ActionRow. + /// The current builder. + public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) + { + if (menu.Options.Distinct().Count() != menu.Options.Count) + throw new InvalidOperationException("Please make sure that there is no duplicates values."); + + var builtMenu = menu.Build(); + + if (Components.Count != 0) + throw new InvalidOperationException($"A Select Menu cannot exist in a pre-occupied ActionRow."); + + AddComponent(builtMenu); + + return this; + } + + /// + /// Adds a with specified parameters to the . + /// + /// The label text for the newly added button. + /// The style of this newly added button. + /// A to be used with this button. + /// The custom id of the newly added button. + /// A URL to be used only if the is a Link. + /// Whether or not the newly created button is disabled. + /// The current builder. + public ActionRowBuilder WithButton( + string label = null, + string customId = null, + ButtonStyle style = ButtonStyle.Primary, + IEmote emote = null, + string url = null, + bool disabled = false) + { + var button = new ButtonBuilder() + .WithLabel(label) + .WithStyle(style) + .WithEmote(emote) + .WithCustomId(customId) + .WithUrl(url) + .WithDisabled(disabled); + + return WithButton(button); + } + + /// + /// Adds a to the . + /// + /// The button to add. + /// Components count reached . + /// A button cannot be added to a row with a SelectMenu. + /// The current builder. + public ActionRowBuilder WithButton(ButtonBuilder button) + { + var builtButton = button.Build(); + + if(Components.Count >= 5) + throw new InvalidOperationException($"Components count reached {MaxChildCount}"); + + if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); + + AddComponent(builtButton); + + return this; + } + /// /// Builds the current builder to a that can be used within a /// @@ -1227,7 +1321,7 @@ namespace Discord /// The text input's minimum length. /// The text input's maximum length. /// The text input's required value. - public TextInputBuilder (string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, + public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int? minLength = null, int? maxLength = null, bool? required = null, string value = null) { Label = label; @@ -1291,7 +1385,7 @@ namespace Discord Placeholder = placeholder; return this; } - + /// /// Sets the value of the current builder. /// From 1f01881bebfc6565ce74c11fe260bcdf15a98cca Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:51:37 +0200 Subject: [PATCH 083/153] feature: Add DefaultArchiveDuration to ITextChannel (#2295) --- .../Entities/Channels/ITextChannel.cs | 11 +++++++++++ src/Discord.Net.Rest/API/Common/Channel.cs | 3 +++ .../Entities/Channels/RestTextChannel.cs | 9 ++++++++- .../Entities/Channels/SocketTextChannel.cs | 8 +++++++- .../MockedEntities/MockedTextChannel.cs | 2 ++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index ae0fe674b..af4e5ec6a 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -35,6 +35,17 @@ namespace Discord /// int SlowModeInterval { get; } + /// + /// Gets the default auto-archive duration for client-created threads in this channel. + /// + /// + /// The value of this property does not affect API thread creation, it will not respect this value. + /// + /// + /// The default auto-archive duration for thread creation in this channel. + /// + ThreadArchiveDuration DefaultArchiveDuration { get; } + /// /// Bulk-deletes multiple messages. /// diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index d565b269a..0eab65686 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -66,5 +66,8 @@ namespace Discord.API [JsonProperty("member_count")] public Optional MemberCount { get; set; } + + [JsonProperty("default_auto_archive_duration")] + public Optional AutoArchiveDuration { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index a73bda334..81f21bcd7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -21,11 +21,12 @@ namespace Discord.Rest public virtual int SlowModeInterval { get; private set; } /// public ulong? CategoryId { get; private set; } - /// public string Mention => MentionUtils.MentionChannel(Id); /// public bool IsNsfw { get; private set; } + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, guild, id) @@ -46,6 +47,12 @@ namespace Discord.Rest if (model.SlowMode.IsSpecified) SlowModeInterval = model.SlowMode.Value; IsNsfw = model.Nsfw.GetValueOrDefault(); + + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e8454ecf8..6aece7d78 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -40,7 +40,8 @@ namespace Discord.WebSocket private bool _nsfw; /// public bool IsNsfw => _nsfw; - + /// + public ThreadArchiveDuration DefaultArchiveDuration { get; private set; } /// public string Mention => MentionUtils.MentionChannel(Id); /// @@ -76,6 +77,11 @@ namespace Discord.WebSocket Topic = model.Topic.GetValueOrDefault(); SlowModeInterval = model.SlowMode.GetValueOrDefault(); // some guilds haven't been patched to include this yet? _nsfw = model.Nsfw.GetValueOrDefault(); + if (model.AutoArchiveDuration.IsSpecified) + DefaultArchiveDuration = model.AutoArchiveDuration.Value; + else + DefaultArchiveDuration = ThreadArchiveDuration.OneDay; + // basic value at channel creation. Shouldn't be called since guild text channels always have this property } /// diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 0dfcab7a5..ab1d3e534 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -10,6 +10,8 @@ namespace Discord { public bool IsNsfw => throw new NotImplementedException(); + public ThreadArchiveDuration DefaultArchiveDuration => throw new NotImplementedException(); + public string Topic => throw new NotImplementedException(); public int SlowModeInterval => throw new NotImplementedException(); From b465d609f08822e58fec10fcf9e60f918b408ca8 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 18 May 2022 10:52:14 +0300 Subject: [PATCH 084/153] fix: Application commands are disabled to everyone except admins by default (#2293) --- src/Discord.Net.Interactions/Info/ModuleInfo.cs | 2 +- .../Utilities/ApplicationCommandRestUtil.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 904d67410..4f40f1607 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -248,7 +248,7 @@ namespace Discord.Interactions while (parent != null) { - permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0); + permissions = (permissions ?? 0) | (parent.DefaultMemberPermissions ?? 0).SanitizeGuildPermissions(); parent = parent.Parent; } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 60980c065..e4b6f893c 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -41,7 +41,7 @@ namespace Discord.Interactions Name = commandInfo.Name, Description = commandInfo.Description, IsDMEnabled = commandInfo.IsEnabledInDm, - DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0) + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), }.Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) @@ -69,14 +69,14 @@ namespace Discord.Interactions { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, - DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0), + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm }.Build(), ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, - DefaultMemberPermissions = (commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0), + DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm }.Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") @@ -232,5 +232,8 @@ namespace Discord.Interactions return builder.Build(); } + + public static GuildPermission? SanitizeGuildPermissions(this GuildPermission permissions) => + permissions == 0 ? null : permissions; } } From 20bd2e9e2f8383fd101c88865d22dea02004280b Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Wed, 18 May 2022 10:52:38 +0300 Subject: [PATCH 085/153] [Docs] Autocomplete examples (#2288) * Improved example in int.framework intro * Added example to `autocompletion` * modified example to utilise user's input * added case insensetive matching; mentioned that 25 suggestions is an API limit --- docs/guides/int_framework/autocompletion.md | 2 ++ .../autocompletion/autocomplete-example.cs | 20 +++++++++++++++++++ .../samples/intro/autocomplete.cs | 18 ++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs diff --git a/docs/guides/int_framework/autocompletion.md b/docs/guides/int_framework/autocompletion.md index 834db2b4f..27da54e36 100644 --- a/docs/guides/int_framework/autocompletion.md +++ b/docs/guides/int_framework/autocompletion.md @@ -18,6 +18,8 @@ AutocompleteHandlers raise the `AutocompleteHandlerExecuted` event on execution. A valid AutocompleteHandlers must inherit [AutocompleteHandler] base type and implement all of its abstract methods. +[!code-csharp[Autocomplete Command Example](samples/autocompletion/autocomplete-example.cs)] + ### GenerateSuggestionsAsync() The Interactions Service uses this method to generate a response of an Autocomplete Interaction. diff --git a/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs new file mode 100644 index 000000000..30c0697e1 --- /dev/null +++ b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs @@ -0,0 +1,20 @@ +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete(typeof(ExampleAutocompleteHandler))] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); + +public class ExampleAutocompleteHandler : AutocompleteHandler +{ + public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + { + // Create a collection with suggestions for autocomplete + IEnumerable results = new[] + { + new AutocompleteResult("Name1", "value111"), + new AutocompleteResult("Name2", "value2") + }; + + // max - 25 suggestions at a time (API limit) + return AutocompletionResult.FromSuccess(results.Take(25)); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/autocomplete.cs b/docs/guides/int_framework/samples/intro/autocomplete.cs index f93c56eaa..11de489f1 100644 --- a/docs/guides/int_framework/samples/intro/autocomplete.cs +++ b/docs/guides/int_framework/samples/intro/autocomplete.cs @@ -1,9 +1,21 @@ [AutocompleteCommand("parameter_name", "command_name")] public async Task Autocomplete() { - IEnumerable results; + string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString(); - ... + IEnumerable results = new[] + { + new AutocompleteResult("foo", "foo_value"), + new AutocompleteResult("bar", "bar_value"), + new AutocompleteResult("baz", "baz_value"), + }.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching - await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results); + + // max - 25 suggestions at a time + await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25)); } + +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); \ No newline at end of file From a24dde4b19adf7a8615f59b9ab9713e5bbb08833 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:56:57 +0200 Subject: [PATCH 086/153] feature: optional API calling to RestInteraction (#2281) * Take 2 * Expose channel & guild Id for manual calling * Make api calling optional at runtime * Resolve build errors * Bind runtime option to interaction type * Expose methods to get channel & guild from API * Patch out NRE's, test on all int types --- src/Discord.Net.Rest/DiscordRestClient.cs | 22 ++- src/Discord.Net.Rest/DiscordRestConfig.cs | 2 + .../CommandBase/RestCommandBase.cs | 8 +- .../CommandBase/RestCommandBaseData.cs | 8 +- .../CommandBase/RestResolvableData.cs | 26 +++- .../MessageCommands/RestMessageCommand.cs | 10 +- .../MessageCommands/RestMessageCommandData.cs | 6 +- .../UserCommands/RestUserCommand.cs | 10 +- .../UserCommands/RestUserCommandData.cs | 4 +- .../MessageComponents/RestMessageComponent.cs | 8 +- .../Entities/Interactions/Modals/RestModal.cs | 4 +- .../Entities/Interactions/RestInteraction.cs | 143 +++++++++++++++--- .../Interactions/RestPingInteraction.cs | 4 +- .../RestAutocompleteInteraction.cs | 4 +- .../SlashCommands/RestSlashCommand.cs | 10 +- .../SlashCommands/RestSlashCommandData.cs | 8 +- .../Entities/Users/RestGuildUser.cs | 14 +- 17 files changed, 212 insertions(+), 79 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index b1948f80a..7cb15bed1 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -32,9 +32,15 @@ namespace Discord.Rest /// Initializes a new with the provided configuration. /// /// The configuration to be used with the client. - public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) { } + public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClient(config)) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } // used for socket client rest access - internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } + internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) + { + _apiOnCreation = config.APIOnRestInteractionCreation; + } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, serializer: Serializer, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); @@ -82,6 +88,8 @@ namespace Discord.Rest #region Rest interactions + private readonly bool _apiOnCreation; + public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, string body) => IsValidHttpInteraction(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); public bool IsValidHttpInteraction(string publicKey, string signature, string timestamp, byte[] body) @@ -113,8 +121,8 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body) - => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body)); + public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func doApiCallOnCreation = null) + => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation); /// /// Creates a from a http message. @@ -127,7 +135,7 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body) + public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func doApiCallOnCreation = null) { if (!IsValidHttpInteraction(publicKey, signature, timestamp, body)) { @@ -138,12 +146,12 @@ namespace Discord.Rest using (var jsonReader = new JsonTextReader(textReader)) { var model = Serializer.Deserialize(jsonReader); - return await RestInteraction.CreateAsync(this, model); + return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation != null ? doApiCallOnCreation(model.Type) : _apiOnCreation); } } #endregion - + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ??= await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 7bf7440ce..a09d9ee98 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -9,5 +9,7 @@ namespace Discord.Rest { /// Gets or sets the provider used to generate new REST connections. public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; + + public bool APIOnRestInteractionCreation { get; set; } = true; } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 196416f0e..22e56a733 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -39,16 +39,16 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestCommandBase(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs index 4227c802a..828299d22 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBaseData.cs @@ -27,20 +27,20 @@ namespace Discord.Rest { } - internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestCommandBaseData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal virtual async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { Name = model.Name; if (model.Resolved.IsSpecified && ResolvableData == null) { ResolvableData = new RestResolvableData(); - await ResolvableData.PopulateAsync(client, guild, channel, model).ConfigureAwait(false); + await ResolvableData.PopulateAsync(client, guild, channel, model, doApiCall).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs index 9353a8530..72b894729 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestResolvableData.cs @@ -22,7 +22,7 @@ namespace Discord.Rest internal readonly Dictionary Attachments = new Dictionary(); - internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model) + internal async Task PopulateAsync(DiscordRestClient discord, RestGuild guild, IRestMessageChannel channel, T model, bool doApiCall) { var resolved = model.Resolved.Value; @@ -38,15 +38,26 @@ namespace Discord.Rest if (resolved.Channels.IsSpecified) { - var channels = await guild.GetChannelsAsync().ConfigureAwait(false); + var channels = doApiCall ? await guild.GetChannelsAsync().ConfigureAwait(false) : null; foreach (var channelModel in resolved.Channels.Value) { - var restChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); + if (channels != null) + { + var guildChannel = channels.FirstOrDefault(x => x.Id == channelModel.Value.Id); - restChannel.Update(channelModel.Value); + guildChannel.Update(channelModel.Value); - Channels.Add(ulong.Parse(channelModel.Key), restChannel); + Channels.Add(ulong.Parse(channelModel.Key), guildChannel); + } + else + { + var restChannel = RestChannel.Create(discord, channelModel.Value); + + restChannel.Update(channelModel.Value); + + Channels.Add(ulong.Parse(channelModel.Key), restChannel); + } } } @@ -76,7 +87,10 @@ namespace Discord.Rest { foreach (var msg in resolved.Messages.Value) { - channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value ?? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false)); + channel ??= (IRestMessageChannel)(Channels.FirstOrDefault(x => x.Key == msg.Value.ChannelId).Value + ?? (doApiCall + ? await discord.GetChannelAsync(msg.Value.ChannelId).ConfigureAwait(false) + : null)); RestUser author; diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs index 609fe0829..34c664b09 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommand.cs @@ -20,22 +20,22 @@ namespace Discord.Rest } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestMessageCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IMessageCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs index 127d539d9..d2968a38a 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/MessageCommands/RestMessageCommandData.cs @@ -23,15 +23,15 @@ namespace Discord.Rest /// Note Not implemented for /// public override IReadOnlyCollection Options - => throw new System.NotImplementedException(); + => throw new NotImplementedException(); internal RestMessageCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestMessageCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs index 7f55fd61b..91319a649 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestUserCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestUserCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //IUserCommandInteractionData diff --git a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs index e18499d42..61b291f7c 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/ContextMenuCommands/UserCommands/RestUserCommandData.cs @@ -26,10 +26,10 @@ namespace Discord.Rest internal RestUserCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestUserCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index 002510eac..e0eab6051 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -37,15 +37,15 @@ namespace Discord.Rest Data = new RestMessageComponentData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestMessageComponent(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient discord, Model model) + internal override async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { - await base.UpdateAsync(discord, model).ConfigureAwait(false); + await base.UpdateAsync(discord, model, doApiCall).ConfigureAwait(false); if (model.Message.IsSpecified && model.ChannelId.IsSpecified) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index 5f54fe051..9229b63b5 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -26,10 +26,10 @@ namespace Discord.Rest Data = new RestModalData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model) + internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) { var entity = new RestModal(client, model); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index b8c0f961d..59adc0347 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -16,6 +16,10 @@ namespace Discord.Rest /// public abstract class RestInteraction : RestEntity, IDiscordInteraction { + // Added so channel & guild methods don't need a client reference + private Func> _getChannel = null; + private Func> _getGuild = null; + /// public InteractionType Type { get; private set; } @@ -31,6 +35,10 @@ namespace Discord.Rest /// /// Gets the user who invoked the interaction. /// + /// + /// If this user is an and is set to false, + /// will return + /// public RestUser User { get; private set; } /// @@ -48,14 +56,38 @@ namespace Discord.Rest public bool IsValidToken => InteractionHelper.CanRespondOrFollowup(this); + /// + /// Gets the ID of the channel this interaction was executed in. + /// + /// + /// if the interaction was not executed in a guild. + /// + public ulong? ChannelId { get; private set; } = null; + /// /// Gets the channel that this interaction was executed in. /// + /// + /// if is set to false. + /// Call to set this property and get the interaction channel. + /// public IRestMessageChannel Channel { get; private set; } /// - /// Gets the guild this interaction was executed in. + /// Gets the ID of the guild this interaction was executed in if applicable. /// + /// + /// if the interaction was not executed in a guild. + /// + public ulong? GuildId { get; private set; } = null; + + /// + /// Gets the guild this interaction was executed in if applicable. + /// + /// + /// This property will be if is set to false + /// or if the interaction was not executed in a guild. + /// public RestGuild Guild { get; private set; } /// @@ -72,11 +104,11 @@ namespace Discord.Rest : DateTime.UtcNow; } - internal static async Task CreateAsync(DiscordRestClient client, Model model) + internal static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { if(model.Type == InteractionType.Ping) { - return await RestPingInteraction.CreateAsync(client, model); + return await RestPingInteraction.CreateAsync(client, model, doApiCall); } if (model.Type == InteractionType.ApplicationCommand) @@ -90,26 +122,26 @@ namespace Discord.Rest return dataModel.Type switch { - ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model).ConfigureAwait(false), - ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model).ConfigureAwait(false), + ApplicationCommandType.Slash => await RestSlashCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.Message => await RestMessageCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), + ApplicationCommandType.User => await RestUserCommand.CreateAsync(client, model, doApiCall).ConfigureAwait(false), _ => null }; } if (model.Type == InteractionType.MessageComponent) - return await RestMessageComponent.CreateAsync(client, model).ConfigureAwait(false); + return await RestMessageComponent.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ApplicationCommandAutocomplete) - return await RestAutocompleteInteraction.CreateAsync(client, model).ConfigureAwait(false); + return await RestAutocompleteInteraction.CreateAsync(client, model, doApiCall).ConfigureAwait(false); if (model.Type == InteractionType.ModalSubmit) - return await RestModal.CreateAsync(client, model).ConfigureAwait(false); + return await RestModal.CreateAsync(client, model, doApiCall).ConfigureAwait(false); return null; } - internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model) + internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { IsDMInteraction = !model.GuildId.IsSpecified; @@ -120,16 +152,23 @@ namespace Discord.Rest Version = model.Version; Type = model.Type; - if(Guild == null && model.GuildId.IsSpecified) + if (Guild == null && model.GuildId.IsSpecified) { - Guild = await discord.GetGuildAsync(model.GuildId.Value); + GuildId = model.GuildId.Value; + if (doApiCall) + Guild = await discord.GetGuildAsync(model.GuildId.Value); + else + { + Guild = null; + _getGuild = new(async (opt, ul) => await discord.GetGuildAsync(ul, opt)); + } } if (User == null) { if (model.Member.IsSpecified && model.GuildId.IsSpecified) { - User = RestGuildUser.Create(Discord, Guild, model.Member.Value); + User = RestGuildUser.Create(Discord, Guild, model.Member.Value, (Guild is null) ? model.GuildId.Value : null); } else { @@ -137,18 +176,33 @@ namespace Discord.Rest } } - if(Channel == null && model.ChannelId.IsSpecified) + if (Channel == null && model.ChannelId.IsSpecified) { try { - Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + ChannelId = model.ChannelId.Value; + if (doApiCall) + Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + else + { + _getChannel = new(async (opt, ul) => + { + if (Guild is null) + return (IRestMessageChannel)await discord.GetChannelAsync(ul, opt); + else // get a guild channel if the guild is set. + return (IRestMessageChannel)await Guild.GetChannelAsync(ul, opt); + }); + + Channel = null; + } } - catch(HttpException x) when(x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore + catch (HttpException x) when (x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore } UserLocale = model.UserLocale.IsSpecified - ? model.UserLocale.Value - : null; + ? model.UserLocale.Value + : null; + GuildLocale = model.GuildLocale.IsSpecified ? model.GuildLocale.Value : null; @@ -164,6 +218,59 @@ namespace Discord.Rest return json.ToString(); } + /// + /// Gets the channel this interaction was executed in. Will be a DM channel if the interaction was executed in DM. + /// + /// + /// Calling this method succesfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// A Rest channel to send messages to. + /// Thrown if no channel can be received. + public async Task GetChannelAsync(RequestOptions options = null) + { + if (IsDMInteraction && Channel is null) + { + var channel = await User.CreateDMChannelAsync(options); + Channel = channel; + } + + else if (Channel is null) + { + var channel = await _getChannel(options, ChannelId.Value); + + if (channel is null) + throw new InvalidOperationException("The interaction channel was not able to be retrieved."); + Channel = channel; + + _getChannel = null; // get rid of it, we don't need it anymore. + } + + return Channel; + } + + /// + /// Gets the guild this interaction was executed in if applicable. + /// + /// + /// Calling this method succesfully will populate the property. + /// After this, further calls to this method will no longer call the API, and depend on the value set in . + /// + /// The request options for this request. + /// The guild this interaction was executed in. if the interaction was executed inside DM. + public async Task GetGuildAsync(RequestOptions options) + { + if (IsDMInteraction) + return null; + + if (Guild is null) + Guild = await _getGuild(options, GuildId.Value); + + _getGuild = null; // get rid of it, we don't need it anymore. + return Guild; + } + /// public abstract string Defer(bool ephemeral = false, RequestOptions options = null); /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs index bd15bc2d3..47e1a3b0f 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestPingInteraction.cs @@ -18,10 +18,10 @@ namespace Discord.Rest { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestPingInteraction(client, model.Id); - await entity.UpdateAsync(client, model); + await entity.UpdateAsync(client, model, doApiCall); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs index 24dbae37a..27c536240 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestAutocompleteInteraction.cs @@ -32,10 +32,10 @@ namespace Discord.Rest Data = new RestAutocompleteInteractionData(dataModel); } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestAutocompleteInteraction(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs index 21184fcf6..f955e7855 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommand.cs @@ -23,22 +23,22 @@ namespace Discord.Rest { } - internal new static async Task CreateAsync(DiscordRestClient client, Model model) + internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) { var entity = new RestSlashCommand(client, model); - await entity.UpdateAsync(client, model).ConfigureAwait(false); + await entity.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { - await base.UpdateAsync(client, model).ConfigureAwait(false); + await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel).ConfigureAwait(false); + Data = await RestSlashCommandData.CreateAsync(client, dataModel, Guild, Channel, doApiCall).ConfigureAwait(false); } //ISlashCommandInteraction diff --git a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs index f967cc628..19a819ab4 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/SlashCommands/RestSlashCommandData.cs @@ -14,15 +14,15 @@ namespace Discord.Rest internal RestSlashCommandData(DiscordRestClient client, Model model) : base(client, model) { } - internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal static new async Task CreateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { var entity = new RestSlashCommandData(client, model); - await entity.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await entity.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); return entity; } - internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel) + internal override async Task UpdateAsync(DiscordRestClient client, Model model, RestGuild guild, IRestMessageChannel channel, bool doApiCall) { - await base.UpdateAsync(client, model, guild, channel).ConfigureAwait(false); + await base.UpdateAsync(client, model, guild, channel, doApiCall).ConfigureAwait(false); Options = model.Options.IsSpecified ? model.Options.Value.Select(x => new RestSlashCommandDataOption(this, x)).ToImmutableArray() diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 0a4a33099..6c311b6b5 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -35,7 +35,7 @@ namespace Discord.Rest /// public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); /// - public ulong GuildId => Guild.Id; + public ulong GuildId { get; } /// public bool? IsPending { get; private set; } /// @@ -80,14 +80,16 @@ namespace Discord.Rest /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) + internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong? guildId = null) : base(discord, id) { - Guild = guild; + if (guild is not null) + Guild = guild; + GuildId = guildId ?? Guild.Id; } - internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model) + internal static RestGuildUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? guildId = null) { - var entity = new RestGuildUser(discord, guild, model.User.Id); + var entity = new RestGuildUser(discord, guild, model.User.Id, guildId); entity.Update(model); return entity; } @@ -116,7 +118,7 @@ namespace Discord.Rest private void UpdateRoles(ulong[] roleIds) { var roles = ImmutableArray.CreateBuilder(roleIds.Length + 1); - roles.Add(Guild.Id); + roles.Add(GuildId); for (int i = 0; i < roleIds.Length; i++) roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); From cea59b55bab36397e8f18b06999797f409ed6086 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 18 May 2022 09:57:37 +0200 Subject: [PATCH 087/153] feature: Add Parse & TryParse to EmbedBuilder & Add ToJsonString extension (#2284) * Add parse & tryparse to embedbuilder. * Add tostring extension for embeds * Modify comments * Resolve suggestions * Update src/Discord.Net.Rest/Extensions/StringExtensions.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- .../Entities/Messages/EmbedBuilder.cs | 50 +++++++++++++++++++ .../Extensions/StringExtensions.cs | 47 +++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/Discord.Net.Rest/Extensions/StringExtensions.cs diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 0304120f5..1e2a7b0d7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Discord.Utils; +using Newtonsoft.Json; namespace Discord { @@ -155,6 +156,55 @@ namespace Discord } } + /// + /// Tries to parse a string into an . + /// + /// The json string to parse. + /// The with populated values. An empty instance if method returns . + /// if was succesfully parsed. if not. + public static bool TryParse(string json, out EmbedBuilder builder) + { + builder = new EmbedBuilder(); + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + { + builder = model.ToEmbedBuilder(); + return true; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Parses a string into an . + /// + /// The json string to parse. + /// An with populated values from the passed . + /// Thrown if the string passed is not valid json. + public static EmbedBuilder Parse(string json) + { + try + { + var model = JsonConvert.DeserializeObject(json); + + if (model is not null) + return model.ToEmbedBuilder(); + + return new EmbedBuilder(); + } + catch + { + throw; + } + } + /// /// Sets the title of an . /// diff --git a/src/Discord.Net.Rest/Extensions/StringExtensions.cs b/src/Discord.Net.Rest/Extensions/StringExtensions.cs new file mode 100644 index 000000000..4981a4298 --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/StringExtensions.cs @@ -0,0 +1,47 @@ +using Discord.Net.Converters; +using Newtonsoft.Json; +using System.Linq; +using System; + +namespace Discord.Rest +{ + /// + /// Responsible for formatting certain entities as Json , to reuse later on. + /// + public static class StringExtensions + { + private static Lazy _settings = new(() => + { + var serializer = new JsonSerializerSettings() + { + ContractResolver = new DiscordContractResolver() + }; + serializer.Converters.Add(new EmbedTypeConverter()); + return serializer; + }); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The builder to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this EmbedBuilder builder, Formatting formatting = Formatting.Indented) + => ToJsonString(builder.Build(), formatting); + + /// + /// Gets a Json formatted from an . + /// + /// + /// See to parse Json back into embed. + /// + /// The embed to format as Json . + /// The formatting in which the Json will be returned. + /// A Json containing the data from the . + public static string ToJsonString(this Embed embed, Formatting formatting = Formatting.Indented) + => JsonConvert.SerializeObject(embed.ToModel(), formatting, _settings.Value); + } +} From 437c8a7f438a67512555e3ab0453d7b1224a2470 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 22 May 2022 08:05:05 -0300 Subject: [PATCH 088/153] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb8437432..e85216dbf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Discord

-Discord NET is an unofficial .NET API Wrapper for the Discord client (https://discord.com). +Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com). ## Documentation From 54a5af7db40188b94adc263141955b8a896f82dc Mon Sep 17 00:00:00 2001 From: Tripletri Date: Mon, 23 May 2022 11:43:38 +0500 Subject: [PATCH 089/153] fix: Upload file size limit (#2313) --- src/Discord.Net.Rest/AssemblyInfo.cs | 1 + .../Entities/Guilds/GuildHelper.cs | 15 +++--- .../Discord.Net.Tests.Integration.csproj | 1 + .../DiscordRestApiClientTests.cs | 53 +++++++++++++++++++ .../Discord.Net.Tests.Unit.csproj | 2 + .../GuildHelperTests.cs | 25 +++++++++ 6 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs create mode 100644 test/Discord.Net.Tests.Unit/GuildHelperTests.cs diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 837fd1d04..59e1f0b4b 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] [assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] +[assembly: InternalsVisibleTo("Discord.Net.Tests.Integration")] [assembly: InternalsVisibleTo("Discord.Net.Interactions")] [assembly: TypeForwardedTo(typeof(Discord.Embed))] diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 469e93db4..8bab35937 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -132,12 +132,15 @@ namespace Discord.Rest } public static ulong GetUploadLimit(IGuild guild) { - return guild.PremiumTier switch + var tierFactor = guild.PremiumTier switch { - PremiumTier.Tier2 => 50ul * 1000000, - PremiumTier.Tier3 => 100ul * 1000000, - _ => 8ul * 1000000 + PremiumTier.Tier2 => 50, + PremiumTier.Tier3 => 100, + _ => 8 }; + + var mebibyte = Math.Pow(2, 20); + return (ulong) (tierFactor * mebibyte); } #endregion @@ -151,7 +154,7 @@ namespace Discord.Rest if (fromUserId.HasValue) return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); - else + else return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); } @@ -908,7 +911,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); - + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, diff --git a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj index 0f399ab68..7b8257bfb 100644 --- a/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj +++ b/test/Discord.Net.Tests.Integration/Discord.Net.Tests.Integration.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs new file mode 100644 index 000000000..96b33b141 --- /dev/null +++ b/test/Discord.Net.Tests.Integration/DiscordRestApiClientTests.cs @@ -0,0 +1,53 @@ +using Discord.API; +using Discord.API.Rest; +using Discord.Net; +using Discord.Rest; +using FluentAssertions; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Discord; + +[CollectionDefinition(nameof(DiscordRestApiClientTests), DisableParallelization = true)] +public class DiscordRestApiClientTests : IClassFixture, IAsyncDisposable +{ + private readonly DiscordRestApiClient _apiClient; + private readonly IGuild _guild; + private readonly ITextChannel _channel; + + public DiscordRestApiClientTests(RestGuildFixture guildFixture) + { + _guild = guildFixture.Guild; + _apiClient = guildFixture.Client.ApiClient; + _channel = _guild.CreateTextChannelAsync("testChannel").Result; + } + + public async ValueTask DisposeAsync() + { + await _channel.DeleteAsync(); + } + + [Fact] + public async Task UploadFile_WithMaximumSize_DontThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild); + using var stream = new MemoryStream(new byte[fileSize]); + + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + } + + [Fact] + public async Task UploadFile_WithOverSize_ThrowsException() + { + var fileSize = GuildHelper.GetUploadLimit(_guild) + 1; + using var stream = new MemoryStream(new byte[fileSize]); + + Func upload = async () => + await _apiClient.UploadFileAsync(_channel.Id, new UploadFileParams(new FileAttachment(stream, "filename"))); + + await upload.Should().ThrowExactlyAsync() + .Where(e => e.DiscordCode == DiscordErrorCode.RequestEntityTooLarge); + } +} diff --git a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj index ec06c3c3d..087a64d83 100644 --- a/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj +++ b/test/Discord.Net.Tests.Unit/Discord.Net.Tests.Unit.csproj @@ -12,7 +12,9 @@ + + all diff --git a/test/Discord.Net.Tests.Unit/GuildHelperTests.cs b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs new file mode 100644 index 000000000..c68f415fe --- /dev/null +++ b/test/Discord.Net.Tests.Unit/GuildHelperTests.cs @@ -0,0 +1,25 @@ +using Discord.Rest; +using FluentAssertions; +using Moq; +using System; +using Xunit; + +namespace Discord; + +public class GuildHelperTests +{ + [Theory] + [InlineData(PremiumTier.None, 8)] + [InlineData(PremiumTier.Tier1, 8)] + [InlineData(PremiumTier.Tier2, 50)] + [InlineData(PremiumTier.Tier3, 100)] + public void GetUploadLimit(PremiumTier tier, ulong factor) + { + var guild = Mock.Of(g => g.PremiumTier == tier); + var expected = factor * (ulong)Math.Pow(2, 20); + + var actual = GuildHelper.GetUploadLimit(guild); + + actual.Should().Be(expected); + } +} From f47f3190d0950cc6314d2bbbf1fed75edf617084 Mon Sep 17 00:00:00 2001 From: sabihoshi <25006819+sabihoshi@users.noreply.github.com> Date: Tue, 24 May 2022 13:29:16 +0800 Subject: [PATCH 090/153] fix: Use IDiscordClient.GetUserAsync impl in DiscordSocketClient (#2319) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 57d58a8b1..b0bd6f621 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -403,7 +403,7 @@ namespace Discord.WebSocket /// the snowflake identifier; null if the user is not found. /// public async ValueTask GetUserAsync(ulong id, RequestOptions options = null) - => await ClientHelper.GetUserAsync(this, id, options).ConfigureAwait(false); + => await ((IDiscordClient)this).GetUserAsync(id, CacheMode.AllowDownload, options).ConfigureAwait(false); /// /// Clears all cached channels from the client. /// From aae549a9764ce71493d9252569d4a1edc4191ac0 Mon Sep 17 00:00:00 2001 From: ShineSyndrome Date: Tue, 24 May 2022 06:29:42 +0100 Subject: [PATCH 091/153] fix: typo in modal section of docs (#2318) --- docs/guides/int_basics/modals/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_basics/modals/intro.md b/docs/guides/int_basics/modals/intro.md index 81f0da03c..3e738c6d8 100644 --- a/docs/guides/int_basics/modals/intro.md +++ b/docs/guides/int_basics/modals/intro.md @@ -99,7 +99,7 @@ When we run the command, our modal should pop up: ### Respond to modals > [!WARNING] -> Modals can not be sent when respoding to a modal. +> Modals can not be sent when responding to a modal. Once a user has submitted the modal, we need to let everyone know what their favorite food is. We can start by hooking a task to the client's From 7a07fd62e41bb62026bfac3bf7cc6697b7e77db2 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue, 24 May 2022 02:30:25 -0300 Subject: [PATCH 092/153] feature: Forum channels (#2316) * initial implementation * Update SocketForumChannel.cs * rest forum channel and remove message builder for 4.x * Update src/Discord.Net.Core/DiscordConfig.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/IForumChannel.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/DiscordConfig.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/IForumChannel.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/IForumChannel.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/IForumChannel.cs Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- src/Discord.Net.Core/DiscordConfig.cs | 10 + .../Entities/Channels/ChannelType.cs | 4 +- .../Entities/Channels/IForumChannel.cs | 216 ++++++++++++++++++ src/Discord.Net.Core/Entities/ForumTag.cs | 42 ++++ src/Discord.Net.Rest/API/Common/Channel.cs | 4 + .../API/Common/ChannelThreads.cs | 3 - src/Discord.Net.Rest/API/Common/ForumTags.cs | 21 ++ .../API/Common/ForumThreadMessage.cs | 33 +++ .../API/Rest/CreateMultipartPostAsync.cs | 96 ++++++++ .../API/Rest/CreatePostParams.cs | 25 ++ .../API/Rest/UploadFileParams.cs | 2 +- .../API/Rest/UploadInteractionFileParams.cs | 2 +- .../API/Rest/UploadWebhookFileParams.cs | 2 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 26 ++- .../Entities/Channels/RestForumChannel.cs | 131 +++++++++++ .../Entities/Channels/RestGuildChannel.cs | 1 + .../Entities/Channels/ThreadHelper.cs | 138 +++++++++++ .../Interactions/InteractionHelper.cs | 4 +- .../Net/Converters/UInt64Converter.cs | 4 +- .../Entities/Channels/SocketForumChannel.cs | 128 +++++++++++ .../Entities/Channels/SocketGuildChannel.cs | 1 + .../Entities/Guilds/SocketGuild.cs | 10 +- 22 files changed, 887 insertions(+), 16 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/IForumChannel.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTag.cs create mode 100644 src/Discord.Net.Rest/API/Common/ForumTags.cs create mode 100644 src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs create mode 100644 src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs create mode 100644 src/Discord.Net.Rest/API/Rest/CreatePostParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 067c55225..2db802f1e 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -132,6 +132,16 @@ namespace Discord /// public const int MaxAuditLogEntriesPerBatch = 100; + /// + /// Returns the max number of stickers that can be sent with a message. + /// + public const int MaxStickersPerMessage = 3; + + /// + /// Returns the max number of embeds that can be sent with a message. + /// + public const int MaxEmbedsPerMessage = 10; + /// /// Gets or sets how a request should act in the case of an error, by default. /// diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs index e60bd5031..15965abc3 100644 --- a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -26,6 +26,8 @@ namespace Discord /// The channel is a stage voice channel. Stage = 13, /// The channel is a guild directory used in hub servers. (Unreleased) - GuildDirectory = 14 + GuildDirectory = 14, + /// The channel is a forum channel containing multiple threads. + Forum = 15 } } diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs new file mode 100644 index 000000000..f4c6da2e2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IForumChannel : IGuildChannel, IMentionable + { + /// + /// Gets a value that indicates whether the channel is NSFW. + /// + /// + /// true if the channel has the NSFW flag enabled; otherwise false. + /// + bool IsNsfw { get; } + + /// + /// Gets the current topic for this text channel. + /// + /// + /// A string representing the topic set in the channel; null if none is set. + /// + string Topic { get; } + + /// + /// Gets the default archive duration for a newly created post. + /// + ThreadArchiveDuration DefaultAutoArchiveDuration { get; } + + /// + /// Gets a collection of tags inside of this forum channel. + /// + IReadOnlyCollection Tags { get; } + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the message. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, + string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The file path of the file. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The of the file to be sent. + /// The name of the attachment. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The attachment containing the file and description. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// A collection of attachments to upload. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// A collection of stickers to send with the file. + /// A array of s to send with this response. Max 10. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// A task that represents the asynchronous creation operation. + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + /// Gets a collection of active threads within this forum channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of active threads. + /// + Task> GetActiveThreadsAsync(RequestOptions options = null); + + /// + /// Gets a collection of publicly archived threads within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of publicly archived threads. + /// + Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads within this forum channel. + /// + /// + /// The bot requires the permission in order to execute this request. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads that the current bot has joined within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTag.cs new file mode 100644 index 000000000..26ae4301e --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTag.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A struct representing a forum channel tag. + /// + public struct ForumTag + { + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + /// Gets the name of the tag. + /// + public string Name { get; } + + /// + /// Gets the emoji of the tag or if none is set. + /// + public IEmote Emoji { get; } + + internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName) + { + if (emojiId.HasValue && emojiId.Value != 0) + Emoji = new Emote(emojiId.Value, emojiName, false); + else if (emojiName != null) + Emoji = new Emoji(name); + else + Emoji = null; + + Id = id; + Name = name; + } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 0eab65686..d9d7d469c 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -67,6 +67,10 @@ namespace Discord.API [JsonProperty("member_count")] public Optional MemberCount { get; set; } + //ForumChannel + [JsonProperty("available_tags")] + public Optional ForumTags { get; set; } + [JsonProperty("default_auto_archive_duration")] public Optional AutoArchiveDuration { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs index 94b2396bf..9fa3e38ce 100644 --- a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs +++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs @@ -9,8 +9,5 @@ namespace Discord.API.Rest [JsonProperty("members")] public ThreadMember[] Members { get; set; } - - [JsonProperty("has_more")] - public bool HasMore { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ForumTags.cs b/src/Discord.Net.Rest/API/Common/ForumTags.cs new file mode 100644 index 000000000..18354e7b2 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumTags.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ForumTags + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs new file mode 100644 index 000000000..132e38e5f --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ForumThreadMessage + { + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("sticker_ids")] + public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs new file mode 100644 index 000000000..0c8bc5494 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -0,0 +1,96 @@ +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreateMultipartPostAsync + { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + public FileAttachment[] Files { get; } + + public string Title { get; set; } + public ThreadArchiveDuration ArchiveDuration { get; set; } + public Optional Slowmode { get; set; } + + + public Optional Content { get; set; } + public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageComponent { get; set; } + public Optional Flags { get; set; } + public Optional Stickers { get; set; } + + public CreateMultipartPostAsync(params FileAttachment[] attachments) + { + Files = attachments; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + + var payload = new Dictionary(); + var message = new Dictionary(); + + payload["name"] = Title; + payload["auto_archive_duration"] = ArchiveDuration; + + if (Slowmode.IsSpecified) + payload["rate_limit_per_user"] = Slowmode.Value; + + // message + if (Content.IsSpecified) + message["content"] = Content.Value; + if (Embeds.IsSpecified) + message["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + message["allowed_mentions"] = AllowedMentions.Value; + if (MessageComponent.IsSpecified) + message["components"] = MessageComponent.Value; + if (Stickers.IsSpecified) + message["sticker_ids"] = Stickers.Value; + if (Flags.IsSpecified) + message["flags"] = Flags.Value; + + List attachments = new(); + + for (int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + message["attachments"] = attachments; + + payload["message"] = message; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + + return d; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs new file mode 100644 index 000000000..974e07c0a --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreatePostParams + { + // thread + [JsonProperty("name")] + public string Title { get; set; } + + [JsonProperty("auto_archive_duration")] + public ThreadArchiveDuration ArchiveDuration { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional Slowmode { get; set; } + + [JsonProperty("message")] + public ForumThreadMessage Message { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 67a690e4d..b85ff646e 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -37,7 +37,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) payload["content"] = Content.Value; if (IsTTS.IsSpecified) - payload["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value; if (Nonce.IsSpecified) payload["nonce"] = Nonce.Value; if (Embeds.IsSpecified) diff --git a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs index f004dec82..ca0f49ccb 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs @@ -50,7 +50,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) data["content"] = Content.Value; if (IsTTS.IsSpecified) - data["tts"] = IsTTS.Value.ToString(); + data["tts"] = IsTTS.Value; if (MessageComponents.IsSpecified) data["components"] = MessageComponents.Value; if (Embeds.IsSpecified) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 1a25e4782..d945d149b 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -36,7 +36,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) payload["content"] = Content.Value; if (IsTTS.IsSpecified) - payload["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value; if (Nonce.IsSpecified) payload["nonce"] = Nonce.Value; if (Username.IsSpecified) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index dcb13d9e3..e179675ba 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -466,6 +466,24 @@ namespace Discord.API #endregion #region Threads + public async Task CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options); + } + + public async Task CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendMultipartAsync("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options); + } + public async Task ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -566,15 +584,15 @@ namespace Discord.API return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); } - public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) + public async Task GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); - var bucket = new BucketIds(channelId: channelId); + var bucket = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); + return await SendAsync("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options); } public async Task GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs new file mode 100644 index 000000000..aff8400aa --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based forum channel in a guild. + /// + public class RestForumChannel : RestGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + + internal RestForumChannel(BaseDiscordClient client, IGuild guild, ulong id) + : base(client, guild, id) + { + + } + + internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestStageChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + base.Update(model); + IsNsfw = model.Nsfw.GetValueOrDefault(false); + Topic = model.Topic.GetValueOrDefault(); + DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + } + + /// + public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + } + + /// + public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index fa2362854..4f9af0335 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -39,6 +39,7 @@ namespace Discord.Rest ChannelType.Text => RestTextChannel.Create(discord, guild, model), ChannelType.Voice => RestVoiceChannel.Create(discord, guild, model), ChannelType.Stage => RestStageChannel.Create(discord, guild, model), + ChannelType.Forum => RestForumChannel.Create(discord, guild, model), ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread => RestThreadChannel.Create(discord, guild, model), _ => new RestGuildChannel(discord, guild, model.Id), diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index e0074ecff..f5fce5a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -1,5 +1,7 @@ using Discord.API.Rest; using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -60,6 +62,33 @@ namespace Discord.Rest return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } + public static async Task> GetActiveThreadsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var result = await client.ApiClient.GetActiveThreadsAsync(guild.Id, options).ConfigureAwait(false); + return result.Threads.Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray(); + } + + public static async Task> GetPublicArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetPublicArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + + public static async Task> GetPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetPrivateArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + + public static async Task> GetJoinedPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetJoinedPrivateArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + public static async Task GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) { var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); @@ -73,5 +102,114 @@ namespace Discord.Rest return RestThreadUser.Create(client, channel.Guild, model, channel); } + + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); + } + + + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + var args = new CreatePostParams() + { + Title = title, + ArchiveDuration = archiveDuration, + Slowmode = slowmode, + Message = new() + { + AllowedMentions = allowedMentions.ToModel(), + Content = text, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + Flags = flags, + Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + } + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); + + return RestThreadChannel.Create(client, channel.Guild, model); + } + + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + { + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); + } + + + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + var args = new CreateMultipartPostAsync(attachments.ToArray()) + { + AllowedMentions = allowedMentions.ToModel(), + ArchiveDuration = archiveDuration, + Content = text, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + Flags = flags, + MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Slowmode = slowmode, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Title = title + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options); + + return RestThreadChannel.Create(client, channel.Guild, model); + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 74d7953ad..522c098e6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -369,7 +369,7 @@ namespace Discord.Rest #endregion #region Responses - public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, + public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, RequestOptions options = null) { var args = new MessageProperties(); @@ -411,7 +411,7 @@ namespace Discord.Rest } public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); - public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, + public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, RequestOptions options = null) { var args = new MessageProperties(); diff --git a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs index 27cbe9290..d7655a30a 100644 --- a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Globalization; @@ -14,7 +14,7 @@ namespace Discord.Net.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + return ulong.Parse(reader.Value?.ToString(), NumberStyles.None, CultureInfo.InvariantCulture); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs new file mode 100644 index 000000000..bc6e28442 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -0,0 +1,128 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a forum channel in a guild. + /// + public class SocketForumChannel : SocketGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + + internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } + + internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketForumChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + IsNsfw = model.Nsfw.GetValueOrDefault(false); + Topic = model.Topic.GetValueOrDefault(); + DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + } + + /// + public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, + AllowedMentions allowedMentions = null, MessageComponent components = null, + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + { + using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + } + + /// + public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, + int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 6d9e759b4..16ed7b32d 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -59,6 +59,7 @@ namespace Discord.WebSocket ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.Create(guild, state, model), ChannelType.Stage => SocketStageChannel.Create(guild, state, model), + ChannelType.Forum => SocketForumChannel.Create(guild, state, model), _ => new SocketGuildChannel(guild.Discord, model.Id, guild), }; } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e12f3d1ef..9ce2f507a 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -705,7 +705,15 @@ namespace Discord.WebSocket /// public SocketThreadChannel GetThreadChannel(ulong id) => GetChannel(id) as SocketThreadChannel; - + /// + /// Gets a forum channel in this guild. + /// + /// The snowflake identifier for the forum channel. + /// + /// A forum channel associated with the specified ; if none is found. + /// + public SocketForumChannel GetForumChannel(ulong id) + => GetChannel(id) as SocketForumChannel; /// /// Gets a voice channel in this guild. /// From 88f6168eeba1fc7f6a2d61f87f4f1b02b30dda40 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Tue, 24 May 2022 02:47:03 -0300 Subject: [PATCH 093/153] fix: NRE with bot scope and user parameters (#2320) --- .../Interaction/SocketBaseCommand/SocketResolvableData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index d722c5a13..a629fd069 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -59,7 +59,7 @@ namespace Discord.WebSocket } } - if (resolved.Members.IsSpecified) + if (resolved.Members.IsSpecified && guild != null) { foreach (var member in resolved.Members.Value) { From ac3b1a4f89eccf087191820e0b6f0c3bccd7efc5 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 24 May 2022 02:56:21 -0300 Subject: [PATCH 094/153] meta: 3.7.0 --- CHANGELOG.md | 21 ++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 023400c80..06a65b020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [3.7.0] - 2022-05-24 +### Added +- #2269 Text-In-Voice (23656e8) +- #2281 Optional API calling to RestInteraction (a24dde4) +- #2283 Support FailIfNotExists on MessageReference (0ec8938) +- #2284 Add Parse & TryParse to EmbedBuilder & Add ToJsonString extension (cea59b5) +- #2289 Add UpdateAsync to SocketModal (b333de2) +- #2291 Webhook support for threads (b0a3b65) +- #2295 Add DefaultArchiveDuration to ITextChannel (1f01881) +- #2296 Add `.With` methods to ActionRowBuilder (13ccc7c) +- #2307 Add Nullable ComponentTypeConverter and TypeReader (6fbd396) +- #2316 Forum channels (7a07fd6) + +### Fixed +- #2290 Possible NRE in Sanitize (20ffa64) +- #2293 Application commands are disabled to everyone except admins by default (b465d60) +- #2299 Close-stage bucketId being null (725d255) +- #2313 Upload file size limit being incorrectly calculated (54a5af7) +- #2319 Use `IDiscordClient.GetUserAsync` impl in `DiscordSocketClient` (f47f319) +- #2320 NRE with bot scope and user parameters (88f6168) + ## [3.6.1] - 2022-04-30 ### Added - #2272 add 50080 Error code (503e720) diff --git a/Discord.Net.targets b/Discord.Net.targets index adb0a338c..51454f9b3 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.6.1 + 3.7.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 105aa0493..ad1e581c6 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.6.1", + "_appFooter": "Discord.Net (c) 2015-2022 3.7.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 3985536f4..fa91043a0 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.6.1$suffix$ + 3.7.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 2c428600b00e4b3f966158203c564313e5fbfb0a Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 24 May 2022 05:16:13 -0300 Subject: [PATCH 095/153] meta: fix target errors --- Discord.Net.targets | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 51454f9b3..2bd761c0d 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -5,8 +5,8 @@ Discord.Net Contributors discord;discordapp https://github.com/Discord-Net/Discord.Net - http://opensource.org/licenses/MIT - https://github.com/Discord-Net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + MIT + PackageLogo.png git git://github.com/Discord-Net/Discord.Net @@ -23,4 +23,7 @@ true true + + + From d3a693ab67c60931a5748d9a2ca999d509f862fc Mon Sep 17 00:00:00 2001 From: d4n Date: Wed, 25 May 2022 04:12:03 -0500 Subject: [PATCH 096/153] feature: Add missing interaction properties (#2325) --- .../Interactions/IDiscordInteraction.cs | 24 +++++ .../Entities/Interactions/RestInteraction.cs | 101 +++++++++--------- .../Entities/Interaction/SocketInteraction.cs | 36 ++++--- 3 files changed, 94 insertions(+), 67 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 9017d310f..a2dbe0e5f 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -52,6 +52,9 @@ namespace Discord /// /// Gets the preferred locale of the invoking User. /// + /// + /// This property returns if the interaction is a REST ping interaction. + /// string UserLocale { get; } /// @@ -67,6 +70,27 @@ namespace Discord /// bool IsDMInteraction { get; } + /// + /// Gets the ID of the channel this interaction was executed in. + /// + /// + /// This property returns if the interaction is a REST ping interaction. + /// + ulong? ChannelId { get; } + + /// + /// Gets the ID of the guild this interaction was executed in. + /// + /// + /// This property returns if the interaction was not executed in a guild. + /// + ulong? GuildId { get; } + + /// + /// Gets the ID of the application this interaction is for. + /// + ulong ApplicationId { get; } + /// /// Responds to an Interaction with type . /// diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 59adc0347..43d13f521 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; using Model = Discord.API.Interaction; @@ -17,8 +16,8 @@ namespace Discord.Rest public abstract class RestInteraction : RestEntity, IDiscordInteraction { // Added so channel & guild methods don't need a client reference - private Func> _getChannel = null; - private Func> _getGuild = null; + private Func> _getChannel; + private Func> _getGuild; /// public InteractionType Type { get; private set; } @@ -56,30 +55,17 @@ namespace Discord.Rest public bool IsValidToken => InteractionHelper.CanRespondOrFollowup(this); - /// - /// Gets the ID of the channel this interaction was executed in. - /// - /// - /// if the interaction was not executed in a guild. - /// - public ulong? ChannelId { get; private set; } = null; - /// /// Gets the channel that this interaction was executed in. /// /// - /// if is set to false. + /// This property will be if is set to false. /// Call to set this property and get the interaction channel. /// public IRestMessageChannel Channel { get; private set; } - /// - /// Gets the ID of the guild this interaction was executed in if applicable. - /// - /// - /// if the interaction was not executed in a guild. - /// - public ulong? GuildId { get; private set; } = null; + /// + public ulong? ChannelId { get; private set; } /// /// Gets the guild this interaction was executed in if applicable. @@ -90,12 +76,18 @@ namespace Discord.Rest /// public RestGuild Guild { get; private set; } + /// + public ulong? GuildId { get; private set; } + /// public bool HasResponded { get; protected set; } /// public bool IsDMInteraction { get; private set; } + /// + public ulong ApplicationId { get; private set; } + internal RestInteraction(BaseDiscordClient discord, ulong id) : base(discord, id) { @@ -143,57 +135,67 @@ namespace Discord.Rest internal virtual async Task UpdateAsync(DiscordRestClient discord, Model model, bool doApiCall) { - IsDMInteraction = !model.GuildId.IsSpecified; + ChannelId = model.ChannelId.IsSpecified + ? model.ChannelId.Value + : null; + + GuildId = model.GuildId.IsSpecified + ? model.GuildId.Value + : null; + + IsDMInteraction = GuildId is null; Data = model.Data.IsSpecified ? model.Data.Value : null; + Token = model.Token; Version = model.Version; Type = model.Type; + ApplicationId = model.ApplicationId; - if (Guild == null && model.GuildId.IsSpecified) + if (Guild is null && GuildId is not null) { - GuildId = model.GuildId.Value; if (doApiCall) - Guild = await discord.GetGuildAsync(model.GuildId.Value); + Guild = await discord.GetGuildAsync(GuildId.Value); else { Guild = null; - _getGuild = new(async (opt, ul) => await discord.GetGuildAsync(ul, opt)); + _getGuild = async (opt, ul) => await discord.GetGuildAsync(ul, opt); } } - if (User == null) + if (User is null) { - if (model.Member.IsSpecified && model.GuildId.IsSpecified) + if (model.Member.IsSpecified && GuildId is not null) { - User = RestGuildUser.Create(Discord, Guild, model.Member.Value, (Guild is null) ? model.GuildId.Value : null); + User = RestGuildUser.Create(Discord, Guild, model.Member.Value, GuildId); } else { User = RestUser.Create(Discord, model.User.Value); } } + - if (Channel == null && model.ChannelId.IsSpecified) + if (Channel is null && ChannelId is not null) { try { - ChannelId = model.ChannelId.Value; if (doApiCall) - Channel = (IRestMessageChannel)await discord.GetChannelAsync(model.ChannelId.Value); + Channel = (IRestMessageChannel)await discord.GetChannelAsync(ChannelId.Value); else { - _getChannel = new(async (opt, ul) => + Channel = null; + + _getChannel = async (opt, ul) => { if (Guild is null) return (IRestMessageChannel)await discord.GetChannelAsync(ul, opt); - else // get a guild channel if the guild is set. - return (IRestMessageChannel)await Guild.GetChannelAsync(ul, opt); - }); - Channel = null; + // get a guild channel if the guild is set. + return (IRestMessageChannel)await Guild.GetChannelAsync(ul, opt); + }; } } catch (HttpException x) when (x.DiscordCode == DiscordErrorCode.MissingPermissions) { } // ignore @@ -222,7 +224,7 @@ namespace Discord.Rest /// Gets the channel this interaction was executed in. Will be a DM channel if the interaction was executed in DM. /// /// - /// Calling this method succesfully will populate the property. + /// Calling this method successfully will populate the property. /// After this, further calls to this method will no longer call the API, and depend on the value set in . /// /// The request options for this request. @@ -230,20 +232,16 @@ namespace Discord.Rest /// Thrown if no channel can be received. public async Task GetChannelAsync(RequestOptions options = null) { - if (IsDMInteraction && Channel is null) + if (Channel is not null) + return Channel; + + if (IsDMInteraction) { - var channel = await User.CreateDMChannelAsync(options); - Channel = channel; + Channel = await User.CreateDMChannelAsync(options); } - - else if (Channel is null) + else if (ChannelId is not null) { - var channel = await _getChannel(options, ChannelId.Value); - - if (channel is null) - throw new InvalidOperationException("The interaction channel was not able to be retrieved."); - Channel = channel; - + Channel = await _getChannel(options, ChannelId.Value) ?? throw new InvalidOperationException("The interaction channel was not able to be retrieved."); _getChannel = null; // get rid of it, we don't need it anymore. } @@ -254,20 +252,19 @@ namespace Discord.Rest /// Gets the guild this interaction was executed in if applicable. /// /// - /// Calling this method succesfully will populate the property. + /// Calling this method successfully will populate the property. /// After this, further calls to this method will no longer call the API, and depend on the value set in . /// /// The request options for this request. /// The guild this interaction was executed in. if the interaction was executed inside DM. public async Task GetGuildAsync(RequestOptions options) { - if (IsDMInteraction) + if (GuildId is null) return null; - if (Guild is null) - Guild = await _getGuild(options, GuildId.Value); - + Guild ??= await _getGuild(options, GuildId.Value); _getGuild = null; // get rid of it, we don't need it anymore. + return Guild; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 5b2da04f5..f8eb6b12e 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -24,20 +24,11 @@ namespace Discord.WebSocket /// public ISocketMessageChannel Channel { get; private set; } - /// - /// Gets the ID of the channel this interaction was used in. - /// - /// - /// This property is exposed in cases where the bot scope is not provided, so the channel entity cannot be retrieved. - ///
- /// To get the channel, you can call - /// as this method makes a request for a if nothing was found in cache. - ///
+ /// public ulong? ChannelId { get; private set; } /// /// Gets the who triggered this interaction. - /// This property will be if the bot scope isn't used. /// public SocketUser User { get; private set; } @@ -74,6 +65,12 @@ namespace Discord.WebSocket /// public bool IsDMInteraction { get; private set; } + /// + public ulong? GuildId { get; private set; } + + /// + public ulong ApplicationId { get; private set; } + internal SocketInteraction(DiscordSocketClient client, ulong id, ISocketMessageChannel channel, SocketUser user) : base(client, id) { @@ -119,13 +116,21 @@ namespace Discord.WebSocket internal virtual void Update(Model model) { - IsDMInteraction = !model.GuildId.IsSpecified; + ChannelId = model.ChannelId.IsSpecified + ? model.ChannelId.Value + : null; - ChannelId = model.ChannelId.ToNullable(); + GuildId = model.GuildId.IsSpecified + ? model.GuildId.Value + : null; + + IsDMInteraction = GuildId is null; + ApplicationId = model.ApplicationId; Data = model.Data.IsSpecified ? model.Data.Value : null; + Token = model.Token; Version = model.Version; Type = model.Type; @@ -133,6 +138,7 @@ namespace Discord.WebSocket UserLocale = model.UserLocale.IsSpecified ? model.UserLocale.Value : null; + GuildLocale = model.GuildLocale.IsSpecified ? model.GuildLocale.Value : null; @@ -392,7 +398,7 @@ namespace Discord.WebSocket /// The request options for this request. /// A task that represents the asynchronous operation of responding to the interaction. public abstract Task RespondWithModalAsync(Modal modal, RequestOptions options = null); - #endregion +#endregion /// /// Attepts to get the channel this interaction was executed in. @@ -416,7 +422,7 @@ namespace Discord.WebSocket catch(HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) { return null; } // bot can't view that channel, return null instead of throwing. } - #region IDiscordInteraction +#region IDiscordInteraction /// IUser IDiscordInteraction.User => User; @@ -446,6 +452,6 @@ namespace Discord.WebSocket async Task IDiscordInteraction.FollowupWithFileAsync(FileAttachment attachment, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(attachment, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); #endif - #endregion +#endregion } } From e1f9b768ae1165e4b799ec29dd86b23772a552d7 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 27 May 2022 08:25:17 -0300 Subject: [PATCH 097/153] fix: NRE with Cacheable.DownloadAsync() (#2331) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b0bd6f621..5743d9abd 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1305,13 +1305,13 @@ namespace Discord.WebSocket user.Update(State, data); - var cacheableBefore = new Cacheable(before, user.Id, true, () => null); + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(null)); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { user = guild.AddOrUpdateUser(data); - var cacheableBefore = new Cacheable(null, user.Id, false, () => null); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult(null)); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } From 712a4aea484865739fe8c39e5690e41d34157a80 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 27 May 2022 13:25:33 +0200 Subject: [PATCH 098/153] fix: voice perms not retaining text perms. (#2329) * Init * Fix switch casting as text and not as voice * rearrange to have voice fall through the switch first Co-authored-by: Quin Lynch --- .../Permissions/ChannelPermissions.cs | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index ee5c9984a..3c6a804c5 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,30 +7,55 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - /// Gets a blank that grants no permissions. - /// A structure that does not contain any set permissions. - public static readonly ChannelPermissions None = new ChannelPermissions(); - /// Gets a that grants all permissions for text channels. - public static readonly ChannelPermissions Text = new ChannelPermissions(0b0_11111_0101100_0000000_1111111110001_010001); - /// Gets a that grants all permissions for voice channels. - public static readonly ChannelPermissions Voice = new ChannelPermissions(0b1_00000_0000100_1111110_0000000011100_010001); - /// Gets a that grants all permissions for stage channels. - public static readonly ChannelPermissions Stage = new ChannelPermissions(0b0_00000_1000100_0111010_0000000010000_010001); - /// Gets a that grants all permissions for category channels. - public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); - /// Gets a that grants all permissions for direct message channels. - public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110001_000000); - /// Gets a that grants all permissions for group channels. - public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); - /// Gets a that grants all permissions for a given channel type. + /// + /// Gets a blank that grants no permissions. + /// + /// + /// A structure that does not contain any set permissions. + /// + public static readonly ChannelPermissions None = new(); + + /// + /// Gets a that grants all permissions for text channels. + /// + public static readonly ChannelPermissions Text = new(0b0_11111_0101100_0000000_1111111110001_010001); + + /// + /// Gets a that grants all permissions for voice channels. + /// + public static readonly ChannelPermissions Voice = new(0b1_11111_0101100_1111110_1111111111101_010001); // (0b1_00000_0000100_1111110_0000000011100_010001 (<- voice only perms) |= Text) + + /// + /// Gets a that grants all permissions for stage channels. + /// + public static readonly ChannelPermissions Stage = new(0b0_00000_1000100_0111010_0000000010000_010001); + + /// + /// Gets a that grants all permissions for category channels. + /// + public static readonly ChannelPermissions Category = new(0b01100_1111110_1111111110001_010001); + + /// + /// Gets a that grants all permissions for direct message channels. + /// + public static readonly ChannelPermissions DM = new(0b00000_1000110_1011100110001_000000); + + /// + /// Gets a that grants all permissions for group channels. + /// + public static readonly ChannelPermissions Group = new(0b00000_1000110_0001101100000_000000); + + /// + /// Gets a that grants all permissions for a given channel type. + /// /// Unknown channel type. public static ChannelPermissions All(IChannel channel) { return channel switch { - ITextChannel _ => Text, IStageChannel _ => Stage, IVoiceChannel _ => Voice, + ITextChannel _ => Text, ICategoryChannel _ => Category, IDMChannel _ => DM, IGroupChannel _ => Group, From a890de93044cb5db12e64fce203e718286d8dd83 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 27 May 2022 13:25:49 +0200 Subject: [PATCH 099/153] feature: better call control in ParseHttpInteraction (#2330) * Init * Fix channelid xmldoc --- src/Discord.Net.Rest/DiscordRestClient.cs | 6 +- .../Interactions/InteractionProperties.cs | 101 ++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 7cb15bed1..daf7287c7 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -121,7 +121,7 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func doApiCallOnCreation = null) + public Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, string body, Func doApiCallOnCreation = null) => ParseHttpInteractionAsync(publicKey, signature, timestamp, Encoding.UTF8.GetBytes(body), doApiCallOnCreation); /// @@ -135,7 +135,7 @@ namespace Discord.Rest /// A that represents the incoming http interaction. /// /// Thrown when the signature doesn't match the public key. - public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func doApiCallOnCreation = null) + public async Task ParseHttpInteractionAsync(string publicKey, string signature, string timestamp, byte[] body, Func doApiCallOnCreation = null) { if (!IsValidHttpInteraction(publicKey, signature, timestamp, body)) { @@ -146,7 +146,7 @@ namespace Discord.Rest using (var jsonReader = new JsonTextReader(textReader)) { var model = Serializer.Deserialize(jsonReader); - return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation != null ? doApiCallOnCreation(model.Type) : _apiOnCreation); + return await RestInteraction.CreateAsync(this, model, doApiCallOnCreation is not null ? doApiCallOnCreation(new InteractionProperties(model)) : _apiOnCreation); } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs new file mode 100644 index 000000000..03750d7d9 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionProperties.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Rest +{ + /// + /// Represents a class that contains data present in all interactions to evaluate against at rest-interaction creation. + /// + public readonly struct InteractionProperties + { + /// + /// The type of this interaction. + /// + public InteractionType Type { get; } + + /// + /// Gets the type of application command this interaction represents. + /// + /// + /// This will be if the is not . + /// + public ApplicationCommandType? CommandType { get; } + + /// + /// Gets the name of the interaction. + /// + /// + /// This will be if the is not . + /// + public string Name { get; } = string.Empty; + + /// + /// Gets the custom ID of the interaction. + /// + /// + /// This will be if the is not or . + /// + public string CustomId { get; } = string.Empty; + + /// + /// Gets the guild ID of the interaction. + /// + /// + /// This will be if this interaction was not executed in a guild. + /// + public ulong? GuildId { get; } + + /// + /// Gets the channel ID of the interaction. + /// + /// + /// This will be if this interaction is . + /// + public ulong? ChannelId { get; } + + internal InteractionProperties(API.Interaction model) + { + Type = model.Type; + CommandType = null; + + if (model.GuildId.IsSpecified) + GuildId = model.GuildId.Value; + else + GuildId = null; + + if (model.ChannelId.IsSpecified) + ChannelId = model.ChannelId.Value; + else + ChannelId = null; + + switch (Type) + { + case InteractionType.ApplicationCommand: + { + var data = (API.ApplicationCommandInteractionData)model.Data; + + CommandType = data.Type; + Name = data.Name; + } + break; + case InteractionType.MessageComponent: + { + var data = (API.MessageComponentInteractionData)model.Data; + + CustomId = data.CustomId; + } + break; + case InteractionType.ModalSubmit: + { + var data = (API.ModalInteractionData)model.Data; + + CustomId = data.CustomId; + } + break; + } + } + } +} From 18dd95442f6382b44bf8ca657bd1264be6df74cf Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Fri, 27 May 2022 08:28:43 -0300 Subject: [PATCH 100/153] [Robot] Add missing json error (#2326) * Add 50600 Error code * Update src/Discord.Net.Core/DiscordErrorCode.cs Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index a6861c10c..b444614e4 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -165,6 +165,7 @@ namespace Discord #endregion #region 2FA (60XXX) + MissingPermissionToSendThisSticker = 50600, Requires2FA = 60003, #endregion From b50afd7d8d077b6fc9ee56212f31f16c7e4ed09c Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Fri, 27 May 2022 08:43:04 -0300 Subject: [PATCH 101/153] meta: 3.7.1 --- CHANGELOG.md | 9 +++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a65b020..b6430bd84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [3.7.1] - 2022-5-27 +### Added +- #2325 Add missing interaction properties (d3a693a) +- #2330 Add better call control in ParseHttpInteraction (a890de9) + +### Fixed +- #2329 Voice perms not retaining text perms. (712a4ae) +- #2331 NRE with Cacheable.DownloadAsync() (e1f9b76) + ## [3.7.0] - 2022-05-24 ### Added - #2269 Text-In-Voice (23656e8) diff --git a/Discord.Net.targets b/Discord.Net.targets index 2bd761c0d..d5c67a692 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.7.0 + 3.7.1 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index ad1e581c6..5c35fcd5f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.7.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.7.1", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index fa91043a0..f7a9bd467 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.7.0$suffix$ + 3.7.1$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 3a37f8914c40f9a7b4f79f0284997ed7a93af54c Mon Sep 17 00:00:00 2001 From: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Date: Thu, 2 Jun 2022 08:12:08 -0500 Subject: [PATCH 102/153] feat: AddOptions no longer has an uneeded restriction, added AddOptions to SlashCommandOptionBuilder (#2338) --- .../SlashCommands/SlashCommandBuilder.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index bf74a160c..d7d086762 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -255,9 +255,6 @@ namespace Discord if (options == null) throw new ArgumentNullException(nameof(options), "Options cannot be null!"); - if (options.Length == 0) - throw new ArgumentException("Options cannot be empty!", nameof(options)); - Options ??= new List(); if (Options.Count + options.Length > MaxOptionsCount) @@ -409,7 +406,7 @@ namespace Discord MinValue = MinValue, MaxValue = MaxValue }; - } + } /// /// Adds an option to the current slash command. @@ -477,6 +474,26 @@ namespace Discord return this; } + /// + /// Adds a collection of options to the current option. + /// + /// The collection of options to add. + /// The current builder. + public SlashCommandOptionBuilder AddOptions(params SlashCommandOptionBuilder[] options) + { + if (options == null) + throw new ArgumentNullException(nameof(options), "Options cannot be null!"); + + if ((Options?.Count ?? 0) + options.Length > SlashCommandBuilder.MaxOptionsCount) + throw new ArgumentOutOfRangeException(nameof(options), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!"); + + foreach (var option in options) + Preconditions.Options(option.Name, option.Description); + + Options.AddRange(options); + return this; + } + /// /// Adds a choice to the current option. /// @@ -640,7 +657,7 @@ namespace Discord MinValue = value; return this; } - + /// /// Sets the current builders max value field. /// From 0fad3e8b37e7b2d231119b19a181a455674babfa Mon Sep 17 00:00:00 2001 From: d4n Date: Thu, 2 Jun 2022 08:12:49 -0500 Subject: [PATCH 103/153] feat: Add method overloads to InteractionService (#2328) --- .../InteractionService.cs | 145 +++++++++++++++--- 1 file changed, 121 insertions(+), 24 deletions(-) diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index f57c75a31..793d89cdc 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -426,17 +426,36 @@ namespace Discord.Interactions /// use . Registering a commands without group names might cause the command traversal to fail. /// /// The target guild. + /// If , this operation will not delete the commands that are missing from . /// Commands to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) { - EnsureClientReady(); - if (guild is null) throw new ArgumentNullException(nameof(guild)); + return await AddCommandsToGuildAsync(guild.Id, deleteMissing, commands).ConfigureAwait(false); + } + + /// + /// Register Application Commands from to a guild. + /// + /// + /// Commands will be registered as standalone commands, if you want the to take effect, + /// use . Registering a commands without group names might cause the command traversal to fail. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Commands to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddCommandsToGuildAsync(ulong guildId, bool deleteMissing = false, params ICommandInfo[] commands) + { + EnsureClientReady(); + var props = new List(); foreach (var command in commands) @@ -456,44 +475,60 @@ namespace Discord.Interactions if (!deleteMissing) { - var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); + var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); } /// /// Register Application Commands from modules provided in to a guild. /// /// The target guild. + /// If , this operation will not delete the commands that are missing from . /// Modules to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) { - EnsureClientReady(); - if (guild is null) throw new ArgumentNullException(nameof(guild)); + return await AddModulesToGuildAsync(guild.Id, deleteMissing, modules).ConfigureAwait(false); + } + + /// + /// Register Application Commands from modules provided in to a guild. + /// + /// The target guild ID. + /// If , this operation will not delete the commands that are missing from . + /// Modules to be registered to Discord. + /// + /// A task representing the command registration process. The task result contains the active application commands of the target guild. + /// + public async Task> AddModulesToGuildAsync(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules) + { + EnsureClientReady(); + var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); if (!deleteMissing) { - var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); + var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } - return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); + return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); } /// /// Register Application Commands from modules provided in as global commands. /// + /// If , this operation will not delete the commands that are missing from . /// Modules to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. @@ -521,6 +556,7 @@ namespace Discord.Interactions /// Commands will be registered as standalone commands, if you want the to take effect, /// use . Registering a commands without group names might cause the command traversal to fail. /// + /// If , this operation will not delete the commands that are missing from . /// Commands to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. @@ -1086,19 +1122,40 @@ namespace Discord.Interactions /// /// The active command permissions after the modification. /// - public async Task ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild, + public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild, params ApplicationCommandPermission[] permissions) { + if (module is null) + throw new ArgumentNullException(nameof(module)); + + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + return await ModifySlashCommandPermissionsAsync(module, guild.Id, permissions).ConfigureAwait(false); + } + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// Module representing the top level Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId, + params ApplicationCommandPermission[] permissions) + { + if (module is null) + throw new ArgumentNullException(nameof(module)); + if (!module.IsSlashGroup) throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); if (!module.IsTopLevelGroup) throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); - if (guild is null) - throw new ArgumentNullException("guild"); - - var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); var appCommand = commands.First(x => x.Name == module.SlashGroupName); return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); @@ -1113,9 +1170,29 @@ namespace Discord.Interactions /// /// The active command permissions after the modification. /// - public async Task ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) => - await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); + public async Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); + + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false); + } + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Slash Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); /// /// Modify the command permissions of the matching Discord Slash Command. @@ -1126,20 +1203,40 @@ namespace Discord.Interactions /// /// The active command permissions after the modification. /// - public async Task ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild, - params ApplicationCommandPermission[] permissions) => - await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); + public async Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild, + params ApplicationCommandPermission[] permissions) + { + if (command is null) + throw new ArgumentNullException(nameof(command)); + + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + return await ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions).ConfigureAwait(false); + } + + /// + /// Modify the command permissions of the matching Discord Slash Command. + /// + /// The Context Command. + /// Target guild ID. + /// New permission values. + /// + /// The active command permissions after the modification. + /// + public async Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId, + params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guildId, permissions).ConfigureAwait(false); - private async Task ModifyApplicationCommandPermissionsAsync (T command, IGuild guild, + private async Task ModifyApplicationCommandPermissionsAsync (T command, ulong guildId, params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo { + if (command is null) + throw new ArgumentNullException(nameof(command)); + if (!command.IsTopLevelCommand) throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); - if (guild is null) - throw new ArgumentNullException("guild"); - - var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); + var commands = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); var appCommand = commands.First(x => x.Name == ( command as IApplicationCommandInfo ).Name); return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); From 7adf516b20d87d04ab55f6c4de82f17a815246a2 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Thu, 2 Jun 2022 10:18:27 -0300 Subject: [PATCH 104/153] fix: Disable TIV restrictions for rollout of TIV (#2342) --- .../Entities/Channels/SocketVoiceChannel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 5fc99c3f1..7bf65d638 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -21,8 +21,12 @@ namespace Discord.WebSocket /// /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. /// - public virtual bool IsTextInVoice - => Guild.Features.HasTextInVoice; + /// + /// Discord currently doesn't have a way to disable Text-In-Voice yet so this field is always + /// on s and on + /// s. + /// + public virtual bool IsTextInVoice => true; /// public int Bitrate { get; private set; } From 35db22e527bdf1b08816aa2ada43c4df508632be Mon Sep 17 00:00:00 2001 From: d4n Date: Thu, 2 Jun 2022 08:19:28 -0500 Subject: [PATCH 105/153] feat: Add support for attachments on interaction response type 7 (#2336) * Add support for attachments on interaction response type 7 * Add missing checks --- .../SocketMessageComponent.cs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index aeff465bd..4f9a769c2 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -226,8 +226,12 @@ namespace Discord.WebSocket bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(Message.Content); bool hasEmbeds = embed.IsSpecified && embed.Value != null || embeds.IsSpecified && embeds.Value?.Length > 0 || Message.Embeds.Any(); + bool hasComponents = args.Components.IsSpecified && args.Components.Value != null; + bool hasAttachments = args.Attachments.IsSpecified; + bool hasFlags = args.Flags.IsSpecified; - if (!hasText && !hasEmbeds) + // No content needed if modifying flags + if ((!hasComponents && !hasText && !hasEmbeds && !hasAttachments) && !hasFlags) Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); var apiEmbeds = embed.IsSpecified || embeds.IsSpecified ? new List() : null; @@ -261,20 +265,41 @@ namespace Discord.WebSocket } } - var response = new API.InteractionResponse + if (!args.Attachments.IsSpecified) { - Type = InteractionResponseType.UpdateMessage, - Data = new API.InteractionCallbackData + var response = new API.InteractionResponse { + Type = InteractionResponseType.UpdateMessage, + Data = new API.InteractionCallbackData + { + Content = args.Content, + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, + Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, + Components = args.Components.IsSpecified + ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() + : Optional.Unspecified, + Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified + } + }; + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + } + else + { + var response = new API.Rest.UploadInteractionFileParams(args.Attachments.Value.ToArray()) + { + Type = InteractionResponseType.UpdateMessage, Content = args.Content, AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value?.ToModel() : Optional.Unspecified, Embeds = apiEmbeds?.ToArray() ?? Optional.Unspecified, - Components = args.Components.IsSpecified + MessageComponents = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Array.Empty() : Optional.Unspecified, Flags = args.Flags.IsSpecified ? args.Flags.Value ?? Optional.Unspecified : Optional.Unspecified - } - }; + }; + + await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); + } lock (_lock) { @@ -284,7 +309,6 @@ namespace Discord.WebSocket } } - await InteractionHelper.SendInteractionResponseAsync(Discord, response, this, Channel, options).ConfigureAwait(false); HasResponded = true; } From 20f0932612c15d9f0f22d6cb40ffff11b42426dc Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Thu, 2 Jun 2022 12:03:38 -0300 Subject: [PATCH 106/153] meta: 3.7.2 --- CHANGELOG.md | 10 ++++- Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6430bd84..a4022e1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog +## [3.7.2] - 2022-06-02 +### Added +- #2328 Add method overloads to InteractionService (0fad3e8) +- #2336 Add support for attachments on interaction response type 7 (35db22e) +- #2338 AddOptions no longer has an uneeded restriction, added AddOptions to SlashCommandOptionBuilder (3a37f89) + +### Fixed +- #2342 Disable TIV restrictions for rollout of TIV (7adf516) -## [3.7.1] - 2022-5-27 +## [3.7.1] - 2022-05-27 ### Added - #2325 Add missing interaction properties (d3a693a) - #2330 Add better call control in ParseHttpInteraction (a890de9) diff --git a/Discord.Net.targets b/Discord.Net.targets index d5c67a692..8cedb40e7 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.7.1 + 3.7.2 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 5c35fcd5f..5dd1e640d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.7.1", + "_appFooter": "Discord.Net (c) 2015-2022 3.7.2", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index f7a9bd467..1a61ff97a 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.7.1$suffix$ + 3.7.2$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 05120f04280344277f7f4ff4766017938e7507fc Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:19:34 +0300 Subject: [PATCH 107/153] Add AutoServiceScopes to IF docs --- docs/guides/int_framework/intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 54e9086a1..23be5b544 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -279,8 +279,8 @@ Meaning, the constructor parameters and public settable properties of a module w For more information on dependency injection, read the [DependencyInjection] guides. > [!NOTE] -> On every command execution, module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net. -> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns. +> On every command execution, if the 'AutoServiceScopes' option is enabled in the config , module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net. +> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns. This doesn't apply to methods other than `ExecuteAsync()`. ## Module Groups From e0d68d47d48b4022c362e7e8bd293fbe0e0d6fd6 Mon Sep 17 00:00:00 2001 From: Wojciech Berdowski <10144015+wberdowski@users.noreply.github.com> Date: Mon, 1 Aug 2022 13:20:48 +0200 Subject: [PATCH 108/153] Add note about voice binaries on linux Makes voice section about precompiled binaries more visible. --- docs/guides/voice/sending-voice.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 555adbca2..36184e3a3 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -17,11 +17,9 @@ bot. (When developing on .NET Framework, this would be `bin/debug`, when developing on .NET Core, this is where you execute `dotnet run` from; typically the same directory as your csproj). -For Windows Users, precompiled binaries are available for your -convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives). +**For Windows users, precompiled binaries are available for your convienence [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).** -For Linux Users, you will need to compile [Sodium] and [Opus] from -source, or install them from your package manager. +**For Linux users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager.** [Sodium]: https://download.libsodium.org/libsodium/releases/ [Opus]: http://downloads.xiph.org/releases/opus/ From ee6e0adf7cd5873c2ca886ec85556d3e8d5656fc Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:23:43 +0300 Subject: [PATCH 109/153] Add RequiredInput to example modal (#2348) - Misha-133 --- docs/guides/int_framework/samples/intro/modal.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs index 65cc81abf..8a6ba9d8a 100644 --- a/docs/guides/int_framework/samples/intro/modal.cs +++ b/docs/guides/int_framework/samples/intro/modal.cs @@ -12,7 +12,9 @@ public class FoodModal : IModal [ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)] public string Food { get; set; } - // Additional paremeters can be specified to further customize the input. + // Additional paremeters can be specified to further customize the input. + // Parameters can be optional + [RequiredInput(false)] [InputLabel("Why??")] [ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)] public string Reason { get; set; } @@ -22,10 +24,15 @@ public class FoodModal : IModal [ModalInteraction("food_menu")] public async Task ModalResponse(FoodModal modal) { + // Check if "Why??" field is populated + string reason = string.IsNullOrWhiteSpace(modal.Reason) + ? "." + : $" because {modal.Reason}"; + // Build the message to send. string message = "hey @everyone, I just learned " + $"{Context.User.Mention}'s favorite food is " + - $"{modal.Food} because {modal.Reason}."; + $"{modal.Food}{reason}"; // Specify the AllowedMentions so we don't actually ping everyone. AllowedMentions mentions = new(); From 06ed99512256125c0d32666906feedc2a323a6da Mon Sep 17 00:00:00 2001 From: misticos <21005901+IvMisticos@users.noreply.github.com> Date: Mon, 1 Aug 2022 13:37:41 +0200 Subject: [PATCH 110/153] docs: Add ServerStarter.Host to deployment.md (#2385) --- docs/guides/deployment/deployment.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/guides/deployment/deployment.md b/docs/guides/deployment/deployment.md index 0491e841d..4313e85b4 100644 --- a/docs/guides/deployment/deployment.md +++ b/docs/guides/deployment/deployment.md @@ -47,6 +47,12 @@ enough. Here is a list of recommended VPS provider. * Location(s): * Europe: Lithuania * Based in: Europe +* [ServerStarter.Host](https://serverstarter.host/clients/store/discord-bots) + * Description: Bot hosting with a panel for quick deployment and + no Linux knowledge required. + * Location(s): + * America: United States + * Based in: United States ## .NET Core Deployment @@ -100,4 +106,4 @@ Windows 10 x64 based machine: * `dotnet publish -c Release -r win10-x64` [.NET Core application deployment]: https://docs.microsoft.com/en-us/dotnet/core/deploying/ -[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog \ No newline at end of file +[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog From cf25acdbc10941003046ef097e996b82acd00e4b Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:39:11 +0300 Subject: [PATCH 111/153] docs: Add IgnoreGroupNames clarification to IF docs (#2374) --- docs/guides/int_framework/intro.md | 5 +++++ docs/guides/int_framework/samples/intro/groupmodule.cs | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 23be5b544..5d3253a6f 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -291,6 +291,11 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can > Although creating nested module stuctures are allowed, > you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy. +> [!NOTE] +> To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute] +> +> However, you have to be careful to prevent overlapping ids of buttons and modals + [!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] ## Executing Commands diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs index f0d992aff..a07b2e4d8 100644 --- a/docs/guides/int_framework/samples/intro/groupmodule.cs +++ b/docs/guides/int_framework/samples/intro/groupmodule.cs @@ -16,6 +16,11 @@ public class CommandGroupModule : InteractionModuleBase await RespondAsync(input); + => await RespondAsync(input, components: new ComponentBuilder().WithButton("Echo", $"echoButton_{input}").Build()); + + // Component interaction with ignoreGroupNames set to true + [ComponentInteraction("echoButton_*", true)] + public async Task EchoButton(string input) + => await RespondAsync(input); } } \ No newline at end of file From 246282dda3a0b093041b2512c64bb5bbd2f9cb3c Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:01:01 +0200 Subject: [PATCH 112/153] docs: Improve IF cmd execution docs (#2405) Adds samples and better explains workflow. --- docs/guides/int_framework/intro.md | 15 ++++++++++++++- docs/guides/int_framework/samples/intro/event.cs | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/guides/int_framework/samples/intro/event.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 5d3253a6f..b51aa8088 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -308,8 +308,19 @@ Any of the following socket events can be used to execute commands: - [AutocompleteExecuted] - [UserCommandExecuted] - [MessageCommandExecuted] +- [ModalExecuted] -Commands can be either executed on the gateway thread or on a seperate thread from the thread pool. This behaviour can be configured by changing the *RunMode* property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute. +These events will trigger for the specific type of interaction they inherit their name from. The [InteractionCreated] event will trigger for all. +An example of executing a command from an event can be seen here: + +[!code-csharp[Command Event Example](samples/intro/event.cs)] + +Commands can be either executed on the gateway thread or on a seperate thread from the thread pool. +This behaviour can be configured by changing the `RunMode` property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute. + +> [!WARNING] +> In the example above, no form of post-execution is presented. +> Please carefully read the [Post Execution Documentation] for the best approach in resolving the result based on your `RunMode`. You can also configure the way [InteractionService] executes the commands. By default, commands are executed using `ConstructorInfo.Invoke()` to create module instances and @@ -364,6 +375,7 @@ delegate can be used to create HTTP responses from a deserialized json object st [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion [DependencyInjection]: xref:Guides.TextCommands.DI +[Post Execution Docuemntation]: xref:Guides.IntFw.PostExecution [GroupAttribute]: xref:Discord.Interactions.GroupAttribute [InteractionService]: xref:Discord.Interactions.InteractionService @@ -376,6 +388,7 @@ delegate can be used to create HTTP responses from a deserialized json object st [AutocompleteExecuted]: xref:Discord.WebSocket.BaseSocketClient [UserCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient [MessageCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient +[ModalExecuted]: xref:Discord.WebSocket.BaseSocketClient [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient [DiscordRestClient]: xref:Discord.Rest.DiscordRestClient [SocketInteractionContext]: xref:Discord.Interactions.SocketInteractionContext diff --git a/docs/guides/int_framework/samples/intro/event.cs b/docs/guides/int_framework/samples/intro/event.cs new file mode 100644 index 000000000..0c9f032b9 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/event.cs @@ -0,0 +1,14 @@ +// Theres multiple ways to subscribe to the event, depending on your application. Please use the approach fit to your type of client. +// DiscordSocketClient: +_socketClient.InteractionCreated += async (x) => +{ + var ctx = new SocketInteractionContext(_socketClient, x); + await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); +} + +// DiscordShardedClient: +_shardedClient.InteractionCreated += async (x) => +{ + var ctx = new ShardedInteractionContext(_shardedClient, x); + await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); +} From 010e8e828ff87212ebe8c2539cdc59c3949fa8ca Mon Sep 17 00:00:00 2001 From: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Date: Mon, 1 Aug 2022 10:38:10 -0500 Subject: [PATCH 113/153] feat: Support the WEBHOOKS_UPDATED event (#2384) Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> --- .../API/Gateway/WebhooksUpdatedEvent.cs | 13 ++++++ .../BaseSocketClient.Events.cs | 41 +++++++++++++------ .../DiscordShardedClient.cs | 2 + .../DiscordSocketClient.cs | 20 +++++++-- 4 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 src/Discord.Net.WebSocket/API/Gateway/WebhooksUpdatedEvent.cs diff --git a/src/Discord.Net.WebSocket/API/Gateway/WebhooksUpdatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/WebhooksUpdatedEvent.cs new file mode 100644 index 000000000..5555dc842 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/WebhooksUpdatedEvent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class WebhooksUpdatedEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index c47591418..21b61c35c 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -106,7 +106,7 @@ namespace Discord.WebSocket /// /// /// This event is fired when a message is deleted. The event handler must return a - /// and accept a and + /// and accept a and /// as its parameters. /// /// @@ -117,11 +117,11 @@ namespace Discord.WebSocket /// /// If caching is enabled via , the /// entity will contain the deleted message; otherwise, in event - /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the /// . /// /// - /// The source channel of the removed message will be passed into the + /// The source channel of the removed message will be passed into the /// parameter. /// /// @@ -143,7 +143,7 @@ namespace Discord.WebSocket /// /// /// This event is fired when multiple messages are bulk deleted. The event handler must return a - /// and accept an and + /// and accept an and /// as its parameters. /// /// @@ -154,11 +154,11 @@ namespace Discord.WebSocket /// /// If caching is enabled via , the /// entity will contain the deleted message; otherwise, in event - /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the /// . /// /// - /// The source channel of the removed message will be passed into the + /// The source channel of the removed message will be passed into the /// parameter. /// /// @@ -178,14 +178,14 @@ namespace Discord.WebSocket /// /// If caching is enabled via , the /// entity will contain the original message; otherwise, in event - /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the /// . /// /// /// The updated message will be passed into the parameter. /// /// - /// The source channel of the updated message will be passed into the + /// The source channel of the updated message will be passed into the /// parameter. /// /// @@ -199,24 +199,24 @@ namespace Discord.WebSocket /// /// /// This event is fired when a reaction is added to a user message. The event handler must return a - /// and accept a , an + /// and accept a , an /// , and a as its parameter. /// /// /// If caching is enabled via , the /// entity will contain the original message; otherwise, in event - /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the /// . /// /// - /// The source channel of the reaction addition will be passed into the + /// The source channel of the reaction addition will be passed into the /// parameter. /// /// /// The reaction that was added will be passed into the parameter. /// /// - /// When fetching the reaction from this event, a user may not be provided under + /// When fetching the reaction from this event, a user may not be provided under /// . Please see the documentation of the property for more /// information. /// @@ -367,7 +367,7 @@ namespace Discord.WebSocket } internal readonly AsyncEvent, SocketGuildEvent, Task>> _guildScheduledEventUpdated = new AsyncEvent, SocketGuildEvent, Task>>(); - + /// /// Fired when a guild event is cancelled. /// @@ -877,5 +877,20 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _guildStickerDeleted = new AsyncEvent>(); #endregion + + #region Webhooks + + /// + /// Fired when a webhook is modified, moved, or deleted. If the webhook was + /// moved the channel represents the destination channel, not the source. + /// + public event Func WebhooksUpdated + { + add { _webhooksUpdated.Add(value); } + remove { _webhooksUpdated.Remove(value); } + } + internal readonly AsyncEvent> _webhooksUpdated = new AsyncEvent>(); + + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 3a14692e0..9fc717762 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -496,6 +496,8 @@ namespace Discord.WebSocket client.GuildScheduledEventStarted += (arg) => _guildScheduledEventStarted.InvokeAsync(arg); client.GuildScheduledEventUserAdd += (arg1, arg2) => _guildScheduledEventUserAdd.InvokeAsync(arg1, arg2); client.GuildScheduledEventUserRemove += (arg1, arg2) => _guildScheduledEventUserRemove.InvokeAsync(arg1, arg2); + + client.WebhooksUpdated += (arg1, arg2) => _webhooksUpdated.InvokeAsync(arg1, arg2); } #endregion diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 5743d9abd..b87b52ffb 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2839,6 +2839,23 @@ namespace Discord.WebSocket #endregion + #region Webhooks + + case "WEBHOOKS_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); + type = "WEBHOOKS_UPDATE"; + await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); + + var guild = State.GetGuild(data.GuildId); + var channel = State.GetChannel(data.ChanelId); + + await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); + } + break; + + #endregion + #region Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); @@ -2858,9 +2875,6 @@ namespace Discord.WebSocket case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); break; - case "WEBHOOKS_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); - break; #endregion #region Others From a663f61a8600a8b6ef88c0af75b0467f538cd4fb Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:41:25 +0200 Subject: [PATCH 114/153] fix: Webhookupdated data naming --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b87b52ffb..3c5621304 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2848,7 +2848,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); var guild = State.GetGuild(data.GuildId); - var channel = State.GetChannel(data.ChanelId); + var channel = State.GetChannel(data.ChannelId); await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel); } From cfd2662963ad0a6d77a587795d2fab3f13b053ab Mon Sep 17 00:00:00 2001 From: zaanposni Date: Mon, 1 Aug 2022 17:43:11 +0200 Subject: [PATCH 115/153] docs: ChannelUpdated event xml comment (#2366) --- src/Discord.Net.WebSocket/BaseSocketClient.Events.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 21b61c35c..fb2110399 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -55,7 +55,7 @@ namespace Discord.WebSocket /// Fired when a channel is updated. /// /// - /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// This event is fired when a generic channel has been updated. The event handler must return a /// and accept 2 as its parameters. /// /// From 902326d1f368aa880c035472b33fcfd62d6c39d3 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:53:22 +0200 Subject: [PATCH 116/153] fix: Range of issues presented by analyzer (#2404) --- .../BuildOverrides.cs | 2 +- .../Entities/Guilds/GuildFeature.cs | 86 +++++++++---------- .../SlashCommands/NullableConverter.cs | 5 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 +- .../DataTypes/ThreadUpdateAuditLogData.cs | 2 +- .../Entities/Guilds/GuildHelper.cs | 2 +- .../Entities/Interactions/RestInteraction.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 9 +- .../SocketBaseCommand/SocketResolvableData.cs | 4 +- 9 files changed, 56 insertions(+), 58 deletions(-) diff --git a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs index 54b56cc60..54bc362ec 100644 --- a/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs +++ b/experiment/Discord.Net.BuildOverrides/BuildOverrides.cs @@ -251,7 +251,7 @@ namespace Discord private static Assembly _overrideDomain_Resolving(AssemblyLoadContext arg1, AssemblyName arg2) { // resolve the override id - var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.FirstOrDefault().FullName)); + var v = _loadedOverrides.FirstOrDefault(x => x.Value.Any(x => x.Assembly.FullName == arg1.Assemblies.First().FullName)); return GetDependencyAsync(v.Key.Id, $"{arg2}").GetAwaiter().GetResult(); } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs index cb57b2726..52a70a6f5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs @@ -12,174 +12,174 @@ namespace Discord /// /// The guild has no features. /// - None = 0, + None = 0L, /// /// The guild has access to animated banners. /// - AnimatedBanner = 1 << 0, + AnimatedBanner = 1L << 0, /// /// The guild has access to set an animated guild icon. /// - AnimatedIcon = 1 << 1, + AnimatedIcon = 1L << 1, /// /// The guild has access to set a guild banner image. /// - Banner = 1 << 2, + Banner = 1L << 2, /// /// The guild has access to channel banners. /// - ChannelBanner = 1 << 3, + ChannelBanner = 1L << 3, /// /// The guild has access to use commerce features (i.e. create store channels). /// - Commerce = 1 << 4, + Commerce = 1L << 4, /// /// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates. /// - Community = 1 << 5, + Community = 1L << 5, /// /// The guild is able to be discovered in the directory. /// - Discoverable = 1 << 6, + Discoverable = 1L << 6, /// /// The guild has discoverable disabled. /// - DiscoverableDisabled = 1 << 7, + DiscoverableDisabled = 1L << 7, /// /// The guild has enabled discoverable before. /// - EnabledDiscoverableBefore = 1 << 8, + EnabledDiscoverableBefore = 1L << 8, /// /// The guild is able to be featured in the directory. /// - Featureable = 1 << 9, + Featureable = 1L << 9, /// /// The guild has a force relay. /// - ForceRelay = 1 << 10, + ForceRelay = 1L << 10, /// /// The guild has a directory entry. /// - HasDirectoryEntry = 1 << 11, + HasDirectoryEntry = 1L << 11, /// /// The guild is a hub. /// - Hub = 1 << 12, + Hub = 1L << 12, /// /// You shouldn't be here... /// - InternalEmployeeOnly = 1 << 13, + InternalEmployeeOnly = 1L << 13, /// /// The guild has access to set an invite splash background. /// - InviteSplash = 1 << 14, + InviteSplash = 1L << 14, /// /// The guild is linked to a hub. /// - LinkedToHub = 1 << 15, + LinkedToHub = 1L << 15, /// /// The guild has member profiles. /// - MemberProfiles = 1 << 16, + MemberProfiles = 1L << 16, /// /// The guild has enabled Membership Screening. /// - MemberVerificationGateEnabled = 1 << 17, + MemberVerificationGateEnabled = 1L << 17, /// /// The guild has enabled monetization. /// - MonetizationEnabled = 1 << 18, + MonetizationEnabled = 1L << 18, /// /// The guild has more emojis. /// - MoreEmoji = 1 << 19, + MoreEmoji = 1L << 19, /// /// The guild has increased custom sticker slots. /// - MoreStickers = 1 << 20, + MoreStickers = 1L << 20, /// /// The guild has access to create news channels. /// - News = 1 << 21, + News = 1L << 21, /// /// The guild has new thread permissions. /// - NewThreadPermissions = 1 << 22, + NewThreadPermissions = 1L << 22, /// /// The guild is partnered. /// - Partnered = 1 << 23, + Partnered = 1L << 23, /// /// The guild has a premium tier three override; guilds made by Discord usually have this. /// - PremiumTier3Override = 1 << 24, + PremiumTier3Override = 1L << 24, /// /// The guild can be previewed before joining via Membership Screening or the directory. /// - PreviewEnabled = 1 << 25, + PreviewEnabled = 1L << 25, /// /// The guild has access to create private threads. /// - PrivateThreads = 1 << 26, + PrivateThreads = 1L << 26, /// /// The guild has relay enabled. /// - RelayEnabled = 1 << 27, + RelayEnabled = 1L << 27, /// /// The guild is able to set role icons. /// - RoleIcons = 1 << 28, + RoleIcons = 1L << 28, /// /// The guild has role subscriptions available for purchase. /// - RoleSubscriptionsAvailableForPurchase = 1 << 29, + RoleSubscriptionsAvailableForPurchase = 1L << 29, /// /// The guild has role subscriptions enabled. /// - RoleSubscriptionsEnabled = 1 << 30, + RoleSubscriptionsEnabled = 1L << 30, /// /// The guild has access to the seven day archive time for threads. /// - SevenDayThreadArchive = 1 << 31, + SevenDayThreadArchive = 1L << 31, /// /// The guild has text in voice enabled. /// - TextInVoiceEnabled = 1 << 32, + TextInVoiceEnabled = 1L << 32, /// /// The guild has threads enabled. /// - ThreadsEnabled = 1 << 33, + ThreadsEnabled = 1L << 33, /// /// The guild has testing threads enabled. /// - ThreadsEnabledTesting = 1 << 34, + ThreadsEnabledTesting = 1L << 34, /// /// The guild has the default thread auto archive. /// - ThreadsDefaultAutoArchiveDuration = 1 << 35, + ThreadsDefaultAutoArchiveDuration = 1L << 35, /// /// The guild has access to the three day archive time for threads. /// - ThreeDayThreadArchive = 1 << 36, + ThreeDayThreadArchive = 1L << 36, /// /// The guild has enabled ticketed events. /// - TicketedEventsEnabled = 1 << 37, + TicketedEventsEnabled = 1L << 37, /// /// The guild has access to set a vanity URL. /// - VanityUrl = 1 << 38, + VanityUrl = 1L << 38, /// /// The guild is verified. /// - Verified = 1 << 39, + Verified = 1L << 39, /// /// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers). /// - VIPRegions = 1 << 40, + VIPRegions = 1L << 40, /// /// The guild has enabled the welcome screen. /// - WelcomeScreenEnabled = 1 << 41, + WelcomeScreenEnabled = 1L << 41, } } diff --git a/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs index 874171175..d85b376d1 100644 --- a/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/SlashCommands/NullableConverter.cs @@ -9,10 +9,11 @@ namespace Discord.Interactions public NullableConverter(InteractionService interactionService, IServiceProvider services) { - var type = Nullable.GetUnderlyingType(typeof(T)); + var nullableType = typeof(T); + var type = Nullable.GetUnderlyingType(nullableType); if (type is null) - throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); + throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {nullableType.FullName}", nameof(type)); _typeConverter = interactionService.GetTypeConverter(type, services); } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index e179675ba..c5b075103 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1756,7 +1756,7 @@ namespace Discord.API if (args.TargetType.Value == TargetUserType.Stream) Preconditions.GreaterThan(args.TargetUserId, 0, nameof(args.TargetUserId)); if (args.TargetType.Value == TargetUserType.EmbeddedApplication) - Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetUserId)); + Preconditions.GreaterThan(args.TargetApplicationId, 0, nameof(args.TargetApplicationId)); } options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ThreadUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ThreadUpdateAuditLogData.cs index 2b9b95418..8eb03114d 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ThreadUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ThreadUpdateAuditLogData.cs @@ -15,7 +15,7 @@ namespace Discord.Rest Thread = thread; ThreadType = type; Before = before; - After = After; + After = after; } internal static ThreadUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 8bab35937..20140994f 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -428,7 +428,7 @@ namespace Discord.Rest var ids = args.Roles.Value.Select(r => r.Id); if (args.RoleIds.IsSpecified) - args.RoleIds.Value.Concat(ids); + args.RoleIds = Optional.Create(args.RoleIds.Value.Concat(ids)); else args.RoleIds = Optional.Create(ids); } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs index 43d13f521..ba2de12a9 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestInteraction.cs @@ -426,7 +426,7 @@ namespace Discord.Rest AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(fileStream, fileName, text, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// - async Task IDiscordInteraction.FollowupWithFileAsync(string filePath, string text, string fileName, Embed[] embeds, bool isTTS, bool ephemeral, + async Task IDiscordInteraction.FollowupWithFileAsync(string filePath, string fileName, string text, Embed[] embeds, bool isTTS, bool ephemeral, AllowedMentions allowedMentions, MessageComponent components, Embed embed, RequestOptions options) => await FollowupWithFileAsync(filePath, text, fileName, embeds, isTTS, ephemeral, allowedMentions, components, embed, options).ConfigureAwait(false); /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 9ce2f507a..cf01857e3 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -532,13 +532,10 @@ namespace Discord.WebSocket Features = model.Features; var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); - if (model.Roles != null) + for (int i = 0; i < model.Roles.Length; i++) { - for (int i = 0; i < model.Roles.Length; i++) - { - var role = SocketRole.Create(this, state, model.Roles[i]); - roles.TryAdd(role.Id, role); - } + var role = SocketRole.Create(this, state, model.Roles[i]); + roles.TryAdd(role.Id, role); } _roles = roles; diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index a629fd069..98a7daefc 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket { foreach (var channel in resolved.Channels.Value) { - SocketChannel socketChannel = guild != null + var socketChannel = guild != null ? guild.GetChannel(channel.Value.Id) : discord.GetChannel(channel.Value.Id); @@ -69,7 +69,7 @@ namespace Discord.WebSocket } } - if (resolved.Roles.IsSpecified) + if (resolved.Roles.IsSpecified && guild != null) { foreach (var role in resolved.Roles.Value) { From 519deda6e75eaccd9a03106b7c50ea0f54e95e99 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 1 Aug 2022 21:46:27 +0200 Subject: [PATCH 117/153] fix: Sharding sample inaccurate (#2408) --- .../Services/InteractionHandlingService.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/samples/ShardedClient/Services/InteractionHandlingService.cs b/samples/ShardedClient/Services/InteractionHandlingService.cs index 3c41d7f33..fc2af8150 100644 --- a/samples/ShardedClient/Services/InteractionHandlingService.cs +++ b/samples/ShardedClient/Services/InteractionHandlingService.cs @@ -22,6 +22,7 @@ namespace ShardedClient.Services _service.Log += LogAsync; _client.InteractionCreated += OnInteractionAsync; + _client.ShardReady += ReadyAsync; // For examples on how to handle post execution, // see the InteractionFramework samples. } @@ -30,11 +31,6 @@ namespace ShardedClient.Services public async Task InitializeAsync() { await _service.AddModulesAsync(typeof(InteractionHandlingService).Assembly, _provider); -#if DEBUG - await _service.RegisterCommandsToGuildAsync(1 /* implement */); -#else - await _service.RegisterCommandsGloballyAsync(); -#endif } private async Task OnInteractionAsync(SocketInteraction interaction) @@ -53,5 +49,14 @@ namespace ShardedClient.Services return Task.CompletedTask; } + + private async Task ReadyAsync(DiscordSocketClient _) + { +#if DEBUG + await _service.RegisterCommandsToGuildAsync(1 /* implement */); +#else + await _service.RegisterCommandsGloballyAsync(); +#endif + } } } From bf493ea04007fcf186bdb3d80e3b6993c25d9eef Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Mon, 1 Aug 2022 21:47:53 +0200 Subject: [PATCH 118/153] feat: Labs deprecation & readme expansion (#2406) --- CONTRIBUTING.md | 3 +- README.md | 83 ++++++++++++++-------- docs/faq/basics/client-basics.md | 2 +- docs/guides/getting_started/installing.md | 20 ++++-- docs/guides/getting_started/labs.md | 30 -------- docs/guides/getting_started/terminology.md | 12 ++-- docs/guides/toc.yml | 3 - docs/guides/v2_v3_guide/v2_to_v3_guide.md | 2 +- 8 files changed, 78 insertions(+), 77 deletions(-) delete mode 100644 docs/guides/getting_started/labs.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 840650cf8..1455267f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,7 @@ following guidelines when possible: ## Development Cycle We prefer all changes to the library to be discussed beforehand, -either in a GitHub issue, or in a discussion in our Discord channel -with library regulars or other contributors. +either in a GitHub issue, or in a discussion in our [Discord server](https://discord.gg/dnet) Issues that are tagged as "up for grabs" are free to be picked up by any member of the community. diff --git a/README.md b/README.md index e85216dbf..7b98a5f41 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@

Discord.Net is an unofficial .NET API Wrapper for the Discord client (https://discord.com). -## Documentation +## 📄 Documentation -- [Nightly](https://discordnet.dev) +- https://discordnet.dev -## Installation +## 📥 Installation ### Stable (NuGet) @@ -33,55 +33,78 @@ Our stable builds available from NuGet through the Discord.Net metapackage: The individual components may also be installed from NuGet: -- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) -- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) -- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) -- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) +- _Webhooks_ + - [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) -### Unstable (MyGet) +- _Text-Command & Interaction services._ + - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) + - [Discord.Net.Interactions](https://www.nuget.org/packages/Discord.Net.Interactions/) -Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). +- _Complete API coverage._ + - [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) + - [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) -### Unstable (Labs) +- _The API core. Implements only entities and barebones functionality._ + - [Discord.Net.Core](https://www.nuget.org/packages/Discord.Net.Core/) -Labs builds are available on nuget (`https://www.nuget.org/packages/Discord.Net.Labs/`) and myget (`https://www.myget.org/F/discord-net-labs/api/v3/index.json`). +### Unstable -## Compiling +Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). +These builds target the dev branch. -In order to compile Discord.Net, you require the following: +## 🛑 Known Issues -### Using Visual Studio +### WebSockets (Win7 and earlier) -- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017) -- [.NET Core SDK](https://www.microsoft.com/net/download/core) +.NET Core 1.1 does not support WebSockets on Win7 and earlier. +This issue has been fixed since the release of .NET Core 2.1. +It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; +alternatively, you may choose to install the +[Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package. -The .NET Core workload must be selected during Visual Studio installation. +### TLS on .NET Framework. -### Using Command Line +Discord supports only TLS1.2+ on all their websites including the API since 07/19/2022. +.NET Framework does not support this protocol by default. +If you depend on .NET Framework, it is suggested to upgrade your project to `net6-windows`. +This framework supports most of the windows-only features introduced by fx, and resolves startup errors from the TLS protocol mismatch. -- [.NET Core SDK](https://www.microsoft.com/net/download/core) +## 🗃️ Versioning Guarantees -## Known Issues +This library generally abides by [Semantic Versioning](https://semver.org). Packages are published in `MAJOR.MINOR.PATCH` version format. -### WebSockets (Win7 and earlier) +### Patch component -.NET Core 1.1 does not support WebSockets on Win7 and earlier. This issue has been fixed since the release of .NET Core 2.1. It is recommended to target .NET Core 2.1 or above for your project if you wish to run your bot on legacy platforms; alternatively, you may choose to install the [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) package. +An increment of the **PATCH** component always indicates that an internal-only change was made, generally a bugfix. These changes will not affect the public-facing API in any way, and are always guaranteed to be forward- and backwards-compatible with your codebase, any pre-compiled dependencies of your codebase. -## Versioning Guarantees +### Minor component -This library generally abides by [Semantic Versioning](https://semver.org). Packages are published in MAJOR.MINOR.PATCH version format. +An increment of the **MINOR** component indicates that some addition was made to the library, +and this addition is not backwards-compatible with prior versions. +However, Discord.Net **does not guarantee forward-compatibility** on minor additions. +In other words, we permit a limited set of breaking changes on a minor version bump. -An increment of the PATCH component always indicates that an internal-only change was made, generally a bugfix. These changes will not affect the public-facing API in any way, and are always guaranteed to be forward- and backwards-compatible with your codebase, any pre-compiled dependencies of your codebase. +Due to the nature of the Discord API, we will oftentimes need to add a property to an entity to support the latest API changes. +Discord.Net provides interfaces as a method of consuming entities; and as such, introducing a new field to an entity is technically a breaking change. +Major version bumps generally indicate some major change to the library, +and as such we are hesitant to bump the major version for every minor addition to the library. +To compromise, we have decided that interfaces should be treated as **consumable only**, +and your applications should typically not be implementing interfaces. -An increment of the MINOR component indicates that some addition was made to the library, and this addition is not backwards-compatible with prior versions. However, Discord.Net **does not guarantee forward-compatibility** on minor additions. In other words, we permit a limited set of breaking changes on a minor version bump. +> For applications where interfaces are implemented, such as in test mocks, we apologize for this inconsistency with SemVer. -Due to the nature of the Discord API, we will oftentimes need to add a property to an entity to support the latest API changes. Discord.Net provides interfaces as a method of consuming entities; and as such, introducing a new field to an entity is technically a breaking change. Major version bumps generally indicate some major change to the library, and as such we are hesitant to bump the major version for every minor addition to the library. To compromise, we have decided that interfaces should be treated as **consumable only**, and your applications should typically not be implementing interfaces. (For applications where interfaces are implemented, such as in test mocks, we apologize for this inconsistency with SemVer). +While we will never break the API (outside of interface changes) on minor builds, +we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord. +As such, a minor version increment may require you to recompile your code, and dependencies, +such as addons, may also need to be recompiled and republished on the newer version. +When a binary breaking change is made, the change will be noted in the release notes. -Furthermore, while we will never break the API (outside of interface changes) on minor builds, we will occasionally need to break the ABI, by introducing parameters to a method to match changes upstream with Discord. As such, a minor version increment may require you to recompile your code, and dependencies, such as addons, may also need to be recompiled and republished on the newer version. When a binary breaking change is made, the change will be noted in the release notes. +### Major component -An increment of the MAJOR component indicates that breaking changes have been made to the library; consumers should check the release notes to determine what changes need to be made. +An increment of the **MAJOR** component indicates that breaking changes have been made to the library; +consumers should check the release notes to determine what changes need to be made. -## Branches +## 📚 Branches ### Release/X.X diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md index ade33c35d..3f21dd165 100644 --- a/docs/faq/basics/client-basics.md +++ b/docs/faq/basics/client-basics.md @@ -36,7 +36,7 @@ _client = new DiscordSocketClient(config); This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages` - GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal]. - GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`. -- All: All intents, it is ill adviced to use this without care, as it *can* cause a memory leak from presence. +- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence. The library will give responsive warnings if you specify unnecessary intents. diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 0dde72380..d07787b67 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -30,17 +30,25 @@ other limitations, you may also consider targeting [.NET Framework] [.net framework]: https://docs.microsoft.com/en-us/dotnet/framework/get-started/ [additional steps]: #installing-on-net-standard-11 -## Installing with NuGet +## Installing Release builds of Discord.Net will be published to the [official NuGet feed]. -Development builds of Discord.Net, as well as add-ons, will be -published to our [MyGet feed]. See -@Guides.GettingStarted.Installation.Nightlies to learn more. +### Experimental/Development -[official nuget feed]: https://nuget.org -[myget feed]: https://www.myget.org/feed/Packages/discord-net +Development builds of Discord.Net will be +published to our [MyGet feed]. The MyGet feed can be used to run the latest dev branch builds. +It is not advised to use MyGet packages in a production environment, as changes may be made that negatively affect certain library functions. + +### Labs + +This exterior branch of Discord.Net has been deprecated and is no longer supported. +If you have used Discord.Net-Labs in the past, you are advised to update to the latest version of Discord.Net. +All features in Labs are implemented in the main repository. + +[official NuGet feed]: https://nuget.org +[MyGet feed]: https://www.myget.org/feed/Packages/discord-net ### [Using Visual Studio](#tab/vs-install) diff --git a/docs/guides/getting_started/labs.md b/docs/guides/getting_started/labs.md deleted file mode 100644 index e52014312..000000000 --- a/docs/guides/getting_started/labs.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -uid: Guides.GettingStarted.Installation.Labs -title: Installing Labs builds ---- - -# Installing Discord.NET Labs - -Discord.NET Labs is the experimental repository that introduces new features & chips away at all bugs until ready for merging into Discord.NET. -Are you looking to test or play with new features? - -> [!IMPORTANT] -> It is very ill advised to use Discord.NET Labs in a production environment normally, -> considering it can include bugs that have not been discovered yet, as features are freshly added. -> However if approached correctly, will work as a pre-release to Discord.NET. -> Make sure to report any bugs at the Labs [repository] or on [Discord] - -[Discord]: https://discord.gg/dnet -[repository]: https://github.com/Discord-Net-Labs/Discord.Net-Labs - -## Installation: - -[NuGet] - This only includes releases, on which features are ready to test. - -> [!NOTE] -> Installing NuGet packages is covered fully at [Installing Discord NET](xref:Guides.GettingStarted.Installation) - -[MyGet] - Available for current builds and unreleased features. - -[NuGet]: https://www.nuget.org/packages/Discord.Net.Labs/ -[MyGet]: https://www.myget.org/feed/Packages/discord-net-labs diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index 0f24edd6f..facf1c790 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -8,18 +8,22 @@ title: Terminology ## Preface Most terms for objects remain the same between 0.9 and 1.0 and above. -The major difference is that the ``Server`` is now called ``Guild`` +The major difference is that the `Server` is now called `Guild` to stay in line with Discord internally. ## Implementation Specific Entities Discord.Net is split into a core library and two different implementations - `Discord.Net.Core`, `Discord.Net.Rest`, and -`Discord.Net.WebSockets`. +`Discord.Net.WebSocket`. -As a bot developer, you will only need to use `Discord.Net.WebSockets`, +You will typically only need to use `Discord.Net.WebSocket`, but you should be aware of the differences between them. +> [!TIP] +> If you are looking to implement Rest based interactions, or handle calls over REST in any other way, +> `Discord.Net.Rest` is the resource most applicable to you. + `Discord.Net.Core` provides a set of interfaces that models Discord's API. These interfaces are consistent throughout all implementations of Discord.Net, and if you are writing an implementation-agnostic library @@ -33,4 +37,4 @@ implementation are prefixed with `Rest` (e.g., `RestChannel`). `Discord.Net.WebSocket` provides a set of concrete classes that are used primarily with Discord's WebSocket API or entities that are kept in cache. When developing bots, you will be using this implementation. -All entities are prefixed with `Socket` (e.g., `SocketChannel`). \ No newline at end of file +All entities are prefixed with `Socket` (e.g., `SocketChannel`). diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index f122ea6ba..45f3983af 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -6,9 +6,6 @@ items: - name: Installation topicUid: Guides.GettingStarted.Installation - items: - - name: Nightly builds - topicUid: Guides.GettingStarted.Installation.Labs - name: Your First Bot topicUid: Guides.GettingStarted.FirstBot - name: Terminology diff --git a/docs/guides/v2_v3_guide/v2_to_v3_guide.md b/docs/guides/v2_v3_guide/v2_to_v3_guide.md index 6b4fa5282..a837f44d2 100644 --- a/docs/guides/v2_v3_guide/v2_to_v3_guide.md +++ b/docs/guides/v2_v3_guide/v2_to_v3_guide.md @@ -38,7 +38,7 @@ _client = new DiscordSocketClient(config); This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages` - GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal]. - GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`. -- All: All intents, it is ill adviced to use this without care, as it _can_ cause a memory leak from presence. +- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence. The library will give responsive warnings if you specify unnecessary intents. > [!NOTE] From 503fa755a011c2fa52ccc76bce74ad712c949aff Mon Sep 17 00:00:00 2001 From: SKProCH <29896317+SKProCH@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:19:31 +0300 Subject: [PATCH 119/153] feat: Add async callbacks for IModuleBase (#2370) --- .../Builders/ModuleClassBuilder.cs | 2 ++ src/Discord.Net.Commands/IModuleBase.cs | 13 +++++++++++++ src/Discord.Net.Commands/ModuleBase.cs | 12 ++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 22c58f5c7..f98c81abd 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -206,6 +206,7 @@ namespace Discord.Commands try { + await instance.BeforeExecuteAsync(cmd).ConfigureAwait(false); instance.BeforeExecute(cmd); var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); @@ -221,6 +222,7 @@ namespace Discord.Commands } finally { + await instance.AfterExecuteAsync(cmd).ConfigureAwait(false); instance.AfterExecute(cmd); (instance as IDisposable)?.Dispose(); } diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs index 8b021f4de..7a953b47b 100644 --- a/src/Discord.Net.Commands/IModuleBase.cs +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -1,4 +1,5 @@ using Discord.Commands.Builders; +using System.Threading.Tasks; namespace Discord.Commands { @@ -13,12 +14,24 @@ namespace Discord.Commands /// The context to set. void SetContext(ICommandContext context); + /// + /// Executed asynchronously before a command is run in this module base. + /// + /// The command thats about to run. + Task BeforeExecuteAsync(CommandInfo command); + /// /// Executed before a command is run in this module base. /// /// The command thats about to run. void BeforeExecute(CommandInfo command); + /// + /// Executed asynchronously after a command is run in this module base. + /// + /// The command thats about to run. + Task AfterExecuteAsync(CommandInfo command); + /// /// Executed after a command is ran in this module base. /// diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 5008cca35..b2d6ba119 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -46,6 +46,11 @@ namespace Discord.Commands return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); } /// + /// The method to execute asynchronously before executing the command. + /// + /// The of the command to be executed. + protected virtual Task BeforeExecuteAsync(CommandInfo command) => Task.CompletedTask; + /// /// The method to execute before executing the command. /// /// The of the command to be executed. @@ -53,6 +58,11 @@ namespace Discord.Commands { } /// + /// The method to execute asynchronously after executing the command. + /// + /// The of the command to be executed. + protected virtual Task AfterExecuteAsync(CommandInfo command) => Task.CompletedTask; + /// /// The method to execute after executing the command. /// /// The of the command to be executed. @@ -76,7 +86,9 @@ namespace Discord.Commands var newValue = context as T; Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}."); } + Task IModuleBase.BeforeExecuteAsync(CommandInfo command) => BeforeExecuteAsync(command); void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); + Task IModuleBase.AfterExecuteAsync(CommandInfo command) => AfterExecuteAsync(command); void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); #endregion From 6fdcf982402e044135babe5407e61a0b5241c4dc Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 2 Aug 2022 11:20:27 +0200 Subject: [PATCH 120/153] docs: Improved DI documentation (#2407) --- docs/guides/dependency_injection/basics.md | 69 ++++++++++++++++++ .../dependency_injection/images/manager.png | Bin 0 -> 12198 bytes docs/guides/dependency_injection/injection.md | 44 +++++++++++ .../samples/access-activator.cs | 9 +++ .../samples/collection.cs | 13 ++++ .../samples/ctor-injecting.cs | 14 ++++ .../samples/enumeration.cs | 18 +++++ .../samples/implicit-registration.cs | 12 +++ .../dependency_injection/samples/modules.cs | 16 ++++ .../dependency_injection/samples/program.cs | 24 ++++++ .../samples/property-injecting.cs | 9 +++ .../dependency_injection/samples/provider.cs | 26 +++++++ .../dependency_injection/samples/runasync.cs | 17 +++++ .../dependency_injection/samples/scoped.cs | 6 ++ .../samples/service-registration.cs | 21 ++++++ .../dependency_injection/samples/services.cs | 9 +++ .../dependency_injection/samples/singleton.cs | 6 ++ .../dependency_injection/samples/transient.cs | 6 ++ docs/guides/dependency_injection/scaling.md | 39 ++++++++++ docs/guides/dependency_injection/services.md | 48 ++++++++++++ docs/guides/dependency_injection/types.md | 52 +++++++++++++ .../int_framework/dependency-injection.md | 13 ---- docs/guides/int_framework/intro.md | 3 +- .../text_commands/dependency-injection.md | 51 ------------- docs/guides/text_commands/intro.md | 2 +- .../dependency_map_setup.cs | 65 ----------------- .../dependency-injection/dependency_module.cs | 37 ---------- .../dependency_module_noinject.cs | 29 -------- 28 files changed, 460 insertions(+), 198 deletions(-) create mode 100644 docs/guides/dependency_injection/basics.md create mode 100644 docs/guides/dependency_injection/images/manager.png create mode 100644 docs/guides/dependency_injection/injection.md create mode 100644 docs/guides/dependency_injection/samples/access-activator.cs create mode 100644 docs/guides/dependency_injection/samples/collection.cs create mode 100644 docs/guides/dependency_injection/samples/ctor-injecting.cs create mode 100644 docs/guides/dependency_injection/samples/enumeration.cs create mode 100644 docs/guides/dependency_injection/samples/implicit-registration.cs create mode 100644 docs/guides/dependency_injection/samples/modules.cs create mode 100644 docs/guides/dependency_injection/samples/program.cs create mode 100644 docs/guides/dependency_injection/samples/property-injecting.cs create mode 100644 docs/guides/dependency_injection/samples/provider.cs create mode 100644 docs/guides/dependency_injection/samples/runasync.cs create mode 100644 docs/guides/dependency_injection/samples/scoped.cs create mode 100644 docs/guides/dependency_injection/samples/service-registration.cs create mode 100644 docs/guides/dependency_injection/samples/services.cs create mode 100644 docs/guides/dependency_injection/samples/singleton.cs create mode 100644 docs/guides/dependency_injection/samples/transient.cs create mode 100644 docs/guides/dependency_injection/scaling.md create mode 100644 docs/guides/dependency_injection/services.md create mode 100644 docs/guides/dependency_injection/types.md delete mode 100644 docs/guides/int_framework/dependency-injection.md delete mode 100644 docs/guides/text_commands/dependency-injection.md delete mode 100644 docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs delete mode 100644 docs/guides/text_commands/samples/dependency-injection/dependency_module.cs delete mode 100644 docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs diff --git a/docs/guides/dependency_injection/basics.md b/docs/guides/dependency_injection/basics.md new file mode 100644 index 000000000..c553ee68c --- /dev/null +++ b/docs/guides/dependency_injection/basics.md @@ -0,0 +1,69 @@ +--- +uid: Guides.DI.Intro +title: Introduction +--- + +# Dependency Injection + +Dependency injection is a feature not required in Discord.Net, but makes it a lot easier to use. +It can be combined with a large number of other libraries, and gives you better control over your application. + +> Further into the documentation, Dependency Injection will be referred to as 'DI'. + +## Installation + +DI is not native to .NET. You need to install the extension packages to your project in order to use it: + +- [Meta](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/). +- [Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/). + +> [!WARNING] +> Downloading the abstractions package alone will not give you access to required classes to use DI properly. +> Please install both packages, or choose to only install the meta package to implicitly install both. + +### Visual Package Manager: + +[Installing](images/manager.png) + +### Command Line: + +`PM> Install-Package Microsoft.Extensions.DependencyInjection`. + +> [!TIP] +> ASP.NET already comes packed with all the necessary assemblies in its framework. +> You do not require to install any additional NuGet packages to make full use of all features of DI in ASP.NET projects. + +## Getting started + +First of all, you will need to create an application based around dependency injection, +which in order will be able to access and inject them across the project. + +[!code-csharp[Building the Program](samples/program.cs)] + +In order to freely pass around your dependencies in different classes, +you will need to register them to a new `ServiceCollection` and build them into an `IServiceProvider` as seen above. +The IServiceProvider then needs to be accessible by the startup file, so you can access your provider and manage them. + +[!code-csharp[Building the Collection](samples/collection.cs)] + +As shown above, an instance of `DiscordSocketConfig` is created, and added **before** the client itself is. +Because the collection will prefer to create the highest populated constructor available with the services already present, +it will prefer the constructor with the configuration, because you already added it. + +## Using your dependencies + +After building your provider in the Program class constructor, the provider is now available inside the instance you're actively using. +Through the provider, we can ask for the DiscordSocketClient we registered earlier. + +[!code-csharp[Applying DI in RunAsync](samples/runasync.cs)] + +> [!WARNING] +> Service constructors are not activated until the service is **first requested**. +> An 'endpoint' service will have to be requested from the provider before it is activated. +> If a service is requested with dependencies, its dependencies (if not already active) will be activated before the service itself is. + +## Injecting dependencies + +You can not only directly access the provider from a field or property, but you can also pass around instances to classes registered in the provider. +There are multiple ways to do this. Please refer to the +[Injection Documentation](Guides.DI.Injection) for further information. diff --git a/docs/guides/dependency_injection/images/manager.png b/docs/guides/dependency_injection/images/manager.png new file mode 100644 index 0000000000000000000000000000000000000000..91791f7a004b1f355cb7af8d274f10044f8bf3fe GIT binary patch literal 12198 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 literal 0 HcmV?d00001 diff --git a/docs/guides/dependency_injection/injection.md b/docs/guides/dependency_injection/injection.md new file mode 100644 index 000000000..c7d40c479 --- /dev/null +++ b/docs/guides/dependency_injection/injection.md @@ -0,0 +1,44 @@ +--- +uid: Guides.DI.Injection +title: Injection +--- + +# Injecting instances within the provider + +You can inject registered services into any class that is registered to the `IServiceProvider`. +This can be done through property or constructor. + +> [!NOTE] +> As mentioned above, the dependency *and* the target class have to be registered in order for the serviceprovider to resolve it. + +## Injecting through a constructor + +Services can be injected from the constructor of the class. +This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class. + +[!code-csharp[Property Injection(samples/property-injecting.cs)]] + +## Injecting through properties + +Injecting through properties is also allowed as follows. + +[!code-csharp[Property Injection](samples/property-injecting.cs)] + +> [!WARNING] +> Dependency Injection will not resolve missing services in property injection, and it will not pick a constructor instead. +> If a publically accessible property is attempted to be injected and its service is missing, the application will throw an error. + +## Using the provider itself + +You can also access the provider reference itself from injecting it into a class. There are multiple use cases for this: + +- Allowing libraries (Like Discord.Net) to access your provider internally. +- Injecting optional dependencies. +- Calling methods on the provider itself if necessary, this is often done for creating scopes. + +[!code-csharp[Provider Injection](samples/provider.cs)] + +> [!NOTE] +> It is important to keep in mind that the provider will pick the 'biggest' available constructor. +> If you choose to introduce multiple constructors, +> keep in mind that services missing from one constructor may have the provider pick another one that *is* available instead of throwing an exception. diff --git a/docs/guides/dependency_injection/samples/access-activator.cs b/docs/guides/dependency_injection/samples/access-activator.cs new file mode 100644 index 000000000..29e71e894 --- /dev/null +++ b/docs/guides/dependency_injection/samples/access-activator.cs @@ -0,0 +1,9 @@ +async Task RunAsync() +{ + //... + + await _serviceProvider.GetRequiredService() + .ActivateAsync(); + + //... +} diff --git a/docs/guides/dependency_injection/samples/collection.cs b/docs/guides/dependency_injection/samples/collection.cs new file mode 100644 index 000000000..4d0457dc9 --- /dev/null +++ b/docs/guides/dependency_injection/samples/collection.cs @@ -0,0 +1,13 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton(); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/ctor-injecting.cs b/docs/guides/dependency_injection/samples/ctor-injecting.cs new file mode 100644 index 000000000..c412bd29c --- /dev/null +++ b/docs/guides/dependency_injection/samples/ctor-injecting.cs @@ -0,0 +1,14 @@ +public class ClientHandler +{ + private readonly DiscordSocketClient _client; + + public ClientHandler(DiscordSocketClient client) + { + _client = client; + } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/enumeration.cs b/docs/guides/dependency_injection/samples/enumeration.cs new file mode 100644 index 000000000..cc8c617f3 --- /dev/null +++ b/docs/guides/dependency_injection/samples/enumeration.cs @@ -0,0 +1,18 @@ +public class ServiceActivator +{ + // This contains *all* registered services of serviceType IService + private readonly IEnumerable _services; + + public ServiceActivator(IEnumerable services) + { + _services = services; + } + + public async Task ActivateAsync() + { + foreach(var service in _services) + { + await service.StartAsync(); + } + } +} diff --git a/docs/guides/dependency_injection/samples/implicit-registration.cs b/docs/guides/dependency_injection/samples/implicit-registration.cs new file mode 100644 index 000000000..52f84228b --- /dev/null +++ b/docs/guides/dependency_injection/samples/implicit-registration.cs @@ -0,0 +1,12 @@ +public static ServiceCollection RegisterImplicitServices(this ServiceCollection collection, Type interfaceType, Type activatorType) +{ + // Get all types in the executing assembly. There are many ways to do this, but this is fastest. + foreach (var type in typeof(Program).Assembly.GetTypes()) + { + if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract) + collection.AddSingleton(interfaceType, type); + } + + // Register the activator so you can activate the instances. + collection.AddSingleton(activatorType); +} diff --git a/docs/guides/dependency_injection/samples/modules.cs b/docs/guides/dependency_injection/samples/modules.cs new file mode 100644 index 000000000..2fadc13d4 --- /dev/null +++ b/docs/guides/dependency_injection/samples/modules.cs @@ -0,0 +1,16 @@ +public class MyModule : InteractionModuleBase +{ + private readonly MyService _service; + + public MyModule(MyService service) + { + _service = service; + } + + [SlashCommand("things", "Shows things")] + public async Task ThingsAsync() + { + var str = string.Join("\n", _service.Things) + await RespondAsync(str); + } +} diff --git a/docs/guides/dependency_injection/samples/program.cs b/docs/guides/dependency_injection/samples/program.cs new file mode 100644 index 000000000..6d985319a --- /dev/null +++ b/docs/guides/dependency_injection/samples/program.cs @@ -0,0 +1,24 @@ +public class Program +{ + private readonly IServiceProvider _serviceProvider; + + public Program() + { + _serviceProvider = CreateProvider(); + } + + static void Main(string[] args) + => new Program().RunAsync(args).GetAwaiter().GetResult(); + + static IServiceProvider CreateProvider() + { + var collection = new ServiceCollection(); + //... + return collection.BuildServiceProvider(); + } + + async Task RunAsync(string[] args) + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/property-injecting.cs b/docs/guides/dependency_injection/samples/property-injecting.cs new file mode 100644 index 000000000..c0c50e150 --- /dev/null +++ b/docs/guides/dependency_injection/samples/property-injecting.cs @@ -0,0 +1,9 @@ +public class ClientHandler +{ + public DiscordSocketClient Client { get; set; } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/provider.cs b/docs/guides/dependency_injection/samples/provider.cs new file mode 100644 index 000000000..26b600b9d --- /dev/null +++ b/docs/guides/dependency_injection/samples/provider.cs @@ -0,0 +1,26 @@ +public class UtilizingProvider +{ + private readonly IServiceProvider _provider; + private readonly AnyService _service; + + // This service is allowed to be null because it is only populated if the service is actually available in the provider. + private readonly AnyOtherService? _otherService; + + // This constructor injects only the service provider, + // and uses it to populate the other dependencies. + public UtilizingProvider(IServiceProvider provider) + { + _provider = provider; + _service = provider.GetRequiredService(); + _otherService = provider.GetService(); + } + + // This constructor injects the service provider, and AnyService, + // making sure that AnyService is not null without having to call GetRequiredService + public UtilizingProvider(IServiceProvider provider, AnyService service) + { + _provider = provider; + _service = service; + _otherService = provider.GetService(); + } +} diff --git a/docs/guides/dependency_injection/samples/runasync.cs b/docs/guides/dependency_injection/samples/runasync.cs new file mode 100644 index 000000000..d24efc83e --- /dev/null +++ b/docs/guides/dependency_injection/samples/runasync.cs @@ -0,0 +1,17 @@ +async Task RunAsync(string[] args) +{ + // Request the instance from the client. + // Because we're requesting it here first, its targetted constructor will be called and we will receive an active instance. + var client = _services.GetRequiredService(); + + client.Log += async (msg) => + { + await Task.CompletedTask; + Console.WriteLine(msg); + } + + await client.LoginAsync(TokenType.Bot, ""); + await client.StartAsync(); + + await Task.Delay(Timeout.Infinite); +} diff --git a/docs/guides/dependency_injection/samples/scoped.cs b/docs/guides/dependency_injection/samples/scoped.cs new file mode 100644 index 000000000..9942f8d8e --- /dev/null +++ b/docs/guides/dependency_injection/samples/scoped.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddScoped(); + +// Without serviceType: +collection.AddScoped(); diff --git a/docs/guides/dependency_injection/samples/service-registration.cs b/docs/guides/dependency_injection/samples/service-registration.cs new file mode 100644 index 000000000..f6e4d22dd --- /dev/null +++ b/docs/guides/dependency_injection/samples/service-registration.cs @@ -0,0 +1,21 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + // X represents either Interaction or Command, as it functions the exact same for both types. + var servConfig = new XServiceConfig() + { + //... + } + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton() + .AddSingleton(servConfig) + .AddSingleton(); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/services.cs b/docs/guides/dependency_injection/samples/services.cs new file mode 100644 index 000000000..2e5235b69 --- /dev/null +++ b/docs/guides/dependency_injection/samples/services.cs @@ -0,0 +1,9 @@ +public class MyService +{ + public List Things { get; } + + public MyService() + { + Things = new(); + } +} diff --git a/docs/guides/dependency_injection/samples/singleton.cs b/docs/guides/dependency_injection/samples/singleton.cs new file mode 100644 index 000000000..f395d743e --- /dev/null +++ b/docs/guides/dependency_injection/samples/singleton.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddSingleton(); + +// Without serviceType: +collection.AddSingleton(); diff --git a/docs/guides/dependency_injection/samples/transient.cs b/docs/guides/dependency_injection/samples/transient.cs new file mode 100644 index 000000000..ae1e1a5d8 --- /dev/null +++ b/docs/guides/dependency_injection/samples/transient.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddTransient(); + +// Without serviceType: +collection.AddTransient(); diff --git a/docs/guides/dependency_injection/scaling.md b/docs/guides/dependency_injection/scaling.md new file mode 100644 index 000000000..356fb7c72 --- /dev/null +++ b/docs/guides/dependency_injection/scaling.md @@ -0,0 +1,39 @@ +--- +uid: Guides.DI.Scaling +title: Scaling your DI +--- + +# Scaling your DI + +Dependency injection has a lot of use cases, and is very suitable for scaled applications. +There are a few ways to make registering & using services easier in large amounts. + +## Using a range of services. + +If you have a lot of services that all have the same use such as handling an event or serving a module, +you can register and inject them all at once by some requirements: + +- All classes need to inherit a single interface or abstract type. +- While not required, it is preferred if the interface and types share a method to call on request. +- You need to register a class that all the types can be injected into. + +### Registering implicitly + +Registering all the types is done through getting all types in the assembly and checking if they inherit the target interface. + +[!code-csharp[Registering](samples/implicit-registration.cs)] + +> [!NOTE] +> As seen above, the interfaceType and activatorType are undefined. For our usecase below, these are `IService` and `ServiceActivator` in order. + +### Using implicit dependencies + +In order to use the implicit dependencies, you have to get access to the activator you registered earlier. + +[!code-csharp[Accessing the activator](samples/access-activator.cs)] + +When the activator is accessed and the `ActivateAsync()` method is called, the following code will be executed: + +[!code-csharp[Executing the activator](samples/enumeration.cs)] + +As a result of this, all the services that were registered with `IService` as its implementation type will execute their starting code, and start up. diff --git a/docs/guides/dependency_injection/services.md b/docs/guides/dependency_injection/services.md new file mode 100644 index 000000000..e021a88be --- /dev/null +++ b/docs/guides/dependency_injection/services.md @@ -0,0 +1,48 @@ +--- +uid: Guides.DI.Services +title: Using DI in Interaction & Command Frameworks +--- + +# DI in the Interaction- & Command Service + +For both the Interaction- and Command Service modules, DI is quite straight-forward to use. + +You can inject any service into modules without the modules having to be registered to the provider. +Discord.Net resolves your dependencies internally. + +> [!WARNING] +> The way DI is used in the Interaction- & Command Service are nearly identical, except for one detail: +> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies) + +## Registering the Service + +Thanks to earlier described behavior of allowing already registered members as parameters of the available ctors, +The socket client & configuration will automatically be acknowledged and the XService(client, config) overload will be used. + +[!code-csharp[Service Registration](samples/service-registration.cs)] + +## Usage in modules + +In the constructor of your module, any parameters will be filled in by +the @System.IServiceProvider that you've passed. + +Any publicly settable properties will also be filled in the same +manner. + +[!code-csharp[Module Injection](samples/modules.cs)] + +If you accept `Command/InteractionService` or `IServiceProvider` as a parameter in your constructor or as an injectable property, +these entries will be filled by the `Command/InteractionService` that the module is loaded from and the `IServiceProvider` that is passed into it respectively. + +> [!NOTE] +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. + +## Services + +Because modules are transient of nature and will reinstantiate on every request, +it is suggested to create a singleton service behind it to hold values across multiple command executions. + +[!code-csharp[Services](samples/services.cs)] + + diff --git a/docs/guides/dependency_injection/types.md b/docs/guides/dependency_injection/types.md new file mode 100644 index 000000000..e539d0695 --- /dev/null +++ b/docs/guides/dependency_injection/types.md @@ -0,0 +1,52 @@ +--- +uid: Guides.DI.Dependencies +title: Types of Dependencies +--- + +# Dependency Types + +There are 3 types of dependencies to learn to use. Several different usecases apply for each. + +> [!WARNING] +> When registering types with a serviceType & implementationType, +> only the serviceType will be available for injection, and the implementationType will be used for the underlying instance. + +## Singleton + +A singleton service creates a single instance when first requested, and maintains that instance across the lifetime of the application. +Any values that are changed within a singleton will be changed across all instances that depend on it, as they all have the same reference to it. + +### Registration: + +[!code-csharp[Singleton Example](samples/singleton.cs)] + +> [!NOTE] +> Types like the Discord client and Interaction/Command services are intended to be singleton, +> as they should last across the entire app and share their state with all references to the object. + +## Scoped + +A scoped service creates a new instance every time a new service is requested, but is kept across the 'scope'. +As long as the service is in view for the created scope, the same instance is used for all references to the type. +This means that you can reuse the same instance during execution, and keep the services' state for as long as the request is active. + +### Registration: + +[!code-csharp[Scoped Example](samples/scoped.cs)] + +> [!NOTE] +> Without using HTTP or libraries like EFCORE, scopes are often unused in Discord bots. +> They are most commonly used for handling HTTP and database requests. + +## Transient + +A transient service is created every time it is requested, and does not share its state between references within the target service. +It is intended for lightweight types that require little state, to be disposed quickly after execution. + +### Registration: + +[!code-csharp[Transient Example](samples/transient.cs)] + +> [!NOTE] +> Discord.Net modules behave exactly as transient types, and are intended to only last as long as the command execution takes. +> This is why it is suggested for apps to use singleton services to keep track of cross-execution data. diff --git a/docs/guides/int_framework/dependency-injection.md b/docs/guides/int_framework/dependency-injection.md deleted file mode 100644 index 31d001f4b..000000000 --- a/docs/guides/int_framework/dependency-injection.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -uid: Guides.IntFw.DI -title: Dependency Injection ---- - -# Dependency Injection - -Dependency injection in the Interaction Service is mostly based on that of the Text-based command service, -for which further information is found [here](xref:Guides.TextCommands.DI). - -> [!NOTE] -> The 2 are nearly identical, except for one detail: -> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index b51aa8088..5cf38bff1 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -374,8 +374,7 @@ delegate can be used to create HTTP responses from a deserialized json object st - Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion -[DependencyInjection]: xref:Guides.TextCommands.DI -[Post Execution Docuemntation]: xref:Guides.IntFw.PostExecution +[DependencyInjection]: xref:Guides.DI.Intro [GroupAttribute]: xref:Discord.Interactions.GroupAttribute [InteractionService]: xref:Discord.Interactions.InteractionService diff --git a/docs/guides/text_commands/dependency-injection.md b/docs/guides/text_commands/dependency-injection.md deleted file mode 100644 index 3253643ef..000000000 --- a/docs/guides/text_commands/dependency-injection.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -uid: Guides.TextCommands.DI -title: Dependency Injection ---- - -# Dependency Injection - -The Text Command Service is bundled with a very barebone Dependency -Injection service for your convenience. It is recommended that you use -DI when writing your modules. - -> [!WARNING] -> If you were brought here from the Interaction Service guides, -> make sure to replace all namespaces that imply `Discord.Commands` with `Discord.Interactions` - -## Setup - -1. Create a @Microsoft.Extensions.DependencyInjection.ServiceCollection. -2. Add the dependencies to the service collection that you wish - to use in the modules. -3. Build the service collection into a service provider. -4. Pass the service collection into @Discord.Commands.CommandService.AddModulesAsync* / @Discord.Commands.CommandService.AddModuleAsync* , @Discord.Commands.CommandService.ExecuteAsync* . - -### Example - Setting up Injection - -[!code-csharp[IServiceProvider Setup](samples/dependency-injection/dependency_map_setup.cs)] - -## Usage in Modules - -In the constructor of your module, any parameters will be filled in by -the @System.IServiceProvider that you've passed. - -Any publicly settable properties will also be filled in the same -manner. - -> [!NOTE] -> Annotating a property with a [DontInjectAttribute] attribute will -> prevent the property from being injected. - -> [!NOTE] -> If you accept `CommandService` or `IServiceProvider` as a parameter -> in your constructor or as an injectable property, these entries will -> be filled by the `CommandService` that the module is loaded from and -> the `IServiceProvider` that is passed into it respectively. - -### Example - Injection in Modules - -[!code-csharp[Injection Modules](samples/dependency-injection/dependency_module.cs)] -[!code-csharp[Disallow Dependency Injection](samples/dependency-injection/dependency_module_noinject.cs)] - -[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute diff --git a/docs/guides/text_commands/intro.md b/docs/guides/text_commands/intro.md index 6632c127a..1113b0821 100644 --- a/docs/guides/text_commands/intro.md +++ b/docs/guides/text_commands/intro.md @@ -187,7 +187,7 @@ service provider. ### Module Constructors -Modules are constructed using [Dependency Injection](xref:Guides.TextCommands.DI). Any parameters +Modules are constructed using [Dependency Injection](xref:Guides.DI.Intro). Any parameters that are placed in the Module's constructor must be injected into an @System.IServiceProvider first. diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs deleted file mode 100644 index 16ca479db..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs +++ /dev/null @@ -1,65 +0,0 @@ -public class Initialize -{ - private readonly CommandService _commands; - private readonly DiscordSocketClient _client; - - // Ask if there are existing CommandService and DiscordSocketClient - // instance. If there are, we retrieve them and add them to the - // DI container; if not, we create our own. - public Initialize(CommandService commands = null, DiscordSocketClient client = null) - { - _commands = commands ?? new CommandService(); - _client = client ?? new DiscordSocketClient(); - } - - public IServiceProvider BuildServiceProvider() => new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(_commands) - // You can pass in an instance of the desired type - .AddSingleton(new NotificationService()) - // ...or by using the generic method. - // - // The benefit of using the generic method is that - // ASP.NET DI will attempt to inject the required - // dependencies that are specified under the constructor - // for us. - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); -} -public class CommandHandler -{ - private readonly DiscordSocketClient _client; - private readonly CommandService _commands; - private readonly IServiceProvider _services; - - public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client) - { - _commands = commands; - _services = services; - _client = client; - } - - public async Task InitializeAsync() - { - // Pass the service provider to the second parameter of - // AddModulesAsync to inject dependencies to all modules - // that may require them. - await _commands.AddModulesAsync( - assembly: Assembly.GetEntryAssembly(), - services: _services); - _client.MessageReceived += HandleCommandAsync; - } - - public async Task HandleCommandAsync(SocketMessage msg) - { - // ... - // Pass the service provider to the ExecuteAsync method for - // precondition checks. - await _commands.ExecuteAsync( - context: context, - argPos: argPos, - services: _services); - // ... - } -} diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs deleted file mode 100644 index 3e42074ca..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_module.cs +++ /dev/null @@ -1,37 +0,0 @@ -// After setting up dependency injection, modules will need to request -// the dependencies to let the library know to pass -// them along during execution. - -// Dependency can be injected in two ways with Discord.Net. -// You may inject any required dependencies via... -// the module constructor -// -or- -// public settable properties - -// Injection via constructor -public class DatabaseModule : ModuleBase -{ - private readonly DatabaseService _database; - public DatabaseModule(DatabaseService database) - { - _database = database; - } - - [Command("read")] - public async Task ReadFromDbAsync() - { - await ReplyAsync(_database.GetData()); - } -} - -// Injection via public settable properties -public class DatabaseModule : ModuleBase -{ - public DatabaseService DbService { get; set; } - - [Command("read")] - public async Task ReadFromDbAsync() - { - await ReplyAsync(DbService.GetData()); - } -} diff --git a/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs b/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs deleted file mode 100644 index 48cd52308..000000000 --- a/docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Sometimes injecting dependencies automatically with the provided -// methods in the prior example may not be desired. - -// You may explicitly tell Discord.Net to **not** inject the properties -// by either... -// restricting the access modifier -// -or- -// applying DontInjectAttribute to the property - -// Restricting the access modifier of the property -public class ImageModule : ModuleBase -{ - public ImageService ImageService { get; } - public ImageModule() - { - ImageService = new ImageService(); - } -} - -// Applying DontInjectAttribute -public class ImageModule : ModuleBase -{ - [DontInject] - public ImageService ImageService { get; set; } - public ImageModule() - { - ImageService = new ImageService(); - } -} From f17866085e308bfdb340fef607fcb406d3e10ada Mon Sep 17 00:00:00 2001 From: Pusheon <59923820+Pusheon@users.noreply.github.com> Date: Tue, 2 Aug 2022 05:24:37 -0400 Subject: [PATCH 121/153] fix: Add DeleteMessagesAsync to IVoiceChannel (#2367) Also adds remaining rate-limit information to client log. --- .../Entities/Channels/IVoiceChannel.cs | 39 +++++++++++++++++++ src/Discord.Net.Rest/BaseDiscordClient.cs | 4 +- .../MockedEntities/MockedVoiceChannel.cs | 3 ++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index d921a2474..d75a4e29c 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -25,6 +26,44 @@ namespace Discord /// int? UserLimit { get; } + /// + /// Bulk-deletes multiple messages. + /// + /// + /// The following example gets 250 messages from the channel and deletes them. + /// + /// var messages = await voiceChannel.GetMessagesAsync(250).FlattenAsync(); + /// await voiceChannel.DeleteMessagesAsync(messages); + /// + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// + /// Bulk-deletes multiple messages. + /// + /// + /// This method attempts to remove the messages specified in bulk. + /// + /// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days! + /// + /// + /// The snowflake identifier of the messages to be bulk-deleted. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous bulk-removal operation. + /// + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); + /// /// Modifies this voice channel. /// diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 75f477c7c..af43e9f4e 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -57,7 +57,7 @@ namespace Discord.Rest if (info == null) await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} Remaining: {info.Value.RetryAfter}s {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } @@ -257,6 +257,6 @@ namespace Discord.Rest /// Task IDiscordClient.StopAsync() => Task.Delay(0); - #endregion + #endregion } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index fdbdeda5e..2ffc75a24 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -12,6 +12,9 @@ namespace Discord public int Bitrate => throw new NotImplementedException(); public int? UserLimit => throw new NotImplementedException(); + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => throw new NotImplementedException(); + + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => throw new NotImplementedException(); public ulong? CategoryId => throw new NotImplementedException(); From ba024164216e8abdec31600e4103e7f3ff89f714 Mon Sep 17 00:00:00 2001 From: Alex Thomson Date: Tue, 2 Aug 2022 21:26:34 +1200 Subject: [PATCH 122/153] fix: DisconnectAsync not disconnecting users (#2346) --- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 2 +- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 974ea69ad..3e0ad1840 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1404,7 +1404,7 @@ namespace Discord.Rest ///
/// The user to disconnect. /// A task that represents the asynchronous operation for disconnecting a user. - async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = null); /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index cf01857e3..78fb33206 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1407,7 +1407,7 @@ namespace Discord.WebSocket ///
/// The user to disconnect. /// A task that represents the asynchronous operation for disconnecting a user. - async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = new Optional()); + async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = null); #endregion #region Stickers From b0b8167efb1b0153913f1251228897c89b178a1f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:54:30 +0200 Subject: [PATCH 123/153] fix: Remove group check from RequireContextAttribute (#2409) --- .../Attributes/Preconditions/RequireContextAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs index 9d1cee8d9..057055ffc 100644 --- a/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Preconditions/RequireContextAttribute.cs @@ -58,7 +58,7 @@ namespace Discord.Interactions if ((Contexts & ContextType.Guild) != 0) isValid = !context.Interaction.IsDMInteraction; - if ((Contexts & ContextType.DM) != 0 && (Contexts & ContextType.Group) != 0) + if ((Contexts & ContextType.DM) != 0) isValid = context.Interaction.IsDMInteraction; if (isValid) From c49d4830af625e57ca3f764f4c962cfad25871b0 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 3 Aug 2022 11:11:26 +0200 Subject: [PATCH 124/153] docs: Fix missing entries in TOC (#2415) --- docs/guides/toc.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 45f3983af..c892eb8c4 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -26,6 +26,18 @@ topicUid: Guides.Entities.Casting - name: Glossary & Flowcharts topicUid: Guides.Entities.Glossary +- name: Dependency Injection + items: + - name: Introduction + topicUid: Guides.DI.Intro + - name: Injection + topicUid: Guides.DI.Injection + - name: Command- & Interaction Services + topicUid: Guides.DI.Services + - name: Service Types + topicUid: Guides.DI.Dependencies + - name: Scaling your Application + topicUid: Guides.DI.Scaling - name: Working with Text-based Commands items: - name: Introduction @@ -36,8 +48,6 @@ topicUid: Guides.TextCommands.NamedArguments - name: Preconditions topicUid: Guides.TextCommands.Preconditions - - name: Dependency Injection - topicUid: Guides.TextCommands.DI - name: Post-execution Handling topicUid: Guides.TextCommands.PostExecution - name: Working with the Interaction Framework @@ -50,8 +60,6 @@ topicUid: Guides.IntFw.TypeConverters - name: Preconditions topicUid: Guides.IntFw.Preconditions - - name: Dependency Injection - topicUid: Guides.IntFw.DI - name: Post-execution Handling topicUid: Guides.IntFw.PostExecution - name: Permissions From 1eb42c6128c295c5dcea076d149a0f9ff13797ec Mon Sep 17 00:00:00 2001 From: d4n Date: Wed, 3 Aug 2022 04:17:43 -0500 Subject: [PATCH 125/153] fix: Issues related to absence of bot scope (#2352) --- .../DiscordSocketClient.cs | 14 ++----- .../Channels/SocketCategoryChannel.cs | 2 +- .../Entities/Channels/SocketForumChannel.cs | 2 +- .../Entities/Channels/SocketNewsChannel.cs | 2 +- .../Entities/Channels/SocketStageChannel.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 4 +- .../Entities/Channels/SocketVoiceChannel.cs | 6 +-- .../MessageCommands/SocketMessageCommand.cs | 4 +- .../UserCommands/SocketUserCommand.cs | 4 +- .../SocketMessageComponent.cs | 4 +- .../SlashCommands/SocketSlashCommand.cs | 4 +- .../SocketApplicationCommand.cs | 2 +- .../SocketBaseCommand/SocketCommandBase.cs | 4 +- .../SocketBaseCommand/SocketResolvableData.cs | 38 ++++++++++++++----- .../Entities/Messages/SocketUserMessage.cs | 2 +- .../Entities/Roles/SocketRole.cs | 2 +- 16 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3c5621304..f0b50aa8f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -2318,7 +2318,7 @@ namespace Discord.WebSocket case "INTERACTION_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - + var data = (payload as JToken).ToObject(_serializer); var guild = data.GuildId.IsSpecified ? GetGuild(data.GuildId.Value) : null; @@ -2326,7 +2326,6 @@ namespace Discord.WebSocket if (guild != null && !guild.IsSynced) { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; } SocketUser user = data.User.IsSpecified @@ -2346,15 +2345,8 @@ namespace Discord.WebSocket { channel = CreateDMChannel(data.ChannelId.Value, user, State); } - else - { - if (guild != null) // The guild id is set, but the guild cannot be found as the bot scope is not set. - { - await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); - return; - } - // The channel isnt required when responding to an interaction, so we can leave the channel null. - } + + // The channel isnt required when responding to an interaction, so we can leave the channel null. } } else if (data.User.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 43f23de1a..42f0c76d4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket } internal new static SocketCategoryChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketCategoryChannel(guild.Discord, model.Id, guild); + var entity = new SocketCategoryChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index bc6e28442..ea58ecdb5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketForumChannel(guild.Discord, model.Id, guild); + var entity = new SocketForumChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs index eed8f9374..81e152530 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket } internal new static SocketNewsChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketNewsChannel(guild.Discord, model.Id, guild); + var entity = new SocketNewsChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs index 56cd92185..4983bc466 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketStageChannel.cs @@ -49,7 +49,7 @@ namespace Discord.WebSocket internal new static SocketStageChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketStageChannel(guild.Discord, model.Id, guild); + var entity = new SocketStageChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 6aece7d78..2d8aeeae7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -61,12 +61,12 @@ namespace Discord.WebSocket internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { - if (Discord.MessageCacheSize > 0) + if (Discord?.MessageCacheSize > 0) _messages = new MessageCache(Discord); } internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketTextChannel(guild.Discord, model.Id, guild); + var entity = new SocketTextChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 7bf65d638..9036659fe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -50,7 +50,7 @@ namespace Discord.WebSocket } internal new static SocketVoiceChannel Create(SocketGuild guild, ClientState state, Model model) { - var entity = new SocketVoiceChannel(guild.Discord, model.Id, guild); + var entity = new SocketVoiceChannel(guild?.Discord, model.Id, guild); entity.Update(state, model); return entity; } @@ -58,8 +58,8 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - Bitrate = model.Bitrate.Value; - UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; + Bitrate = model.Bitrate.GetValueOrDefault(64000); + UserLimit = model.UserLimit.GetValueOrDefault() != 0 ? model.UserLimit.Value : (int?)null; RTCRegion = model.RTCRegion.GetValueOrDefault(null); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs index ad5575caa..0c473bcdd 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/MessageCommands/SocketMessageCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketMessageCommandData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs index c33c06f83..70e06f273 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/ContextMenuCommands/UserCommands/SocketUserCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketUserCommandData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 4f9a769c2..2a1a67d04 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -61,7 +61,9 @@ namespace Discord.WebSocket author = channel.Guild.GetUser(model.Message.Value.Author.Value.Id); } else if (model.Message.Value.Author.IsSpecified) - author = (Channel as SocketChannel).GetUser(model.Message.Value.Author.Value.Id); + author = (Channel as SocketChannel)?.GetUser(model.Message.Value.Author.Value.Id); + + author ??= Discord.State.GetOrAddUser(model.Message.Value.Author.Value.Id, _ => SocketGlobalUser.Create(Discord, Discord.State, model.Message.Value.Author.Value)); Message = SocketUserMessage.Create(Discord, Discord.State, author, Channel, model.Message.Value); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs index b3aa4a826..69f733e85 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SlashCommands/SocketSlashCommand.cs @@ -20,9 +20,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketSlashCommandData.Create(client, dataModel, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index 8f27b65f4..f6b3f9699 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -19,7 +19,7 @@ namespace Discord.WebSocket /// Gets whether or not this command is a global application command. ///
public bool IsGlobalCommand - => Guild == null; + => GuildId is null; /// public ulong ApplicationId { get; private set; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 273f27c9c..bdab128f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -43,9 +43,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - ulong? guildId = null; - if (Channel is SocketGuildChannel guildChannel) - guildId = guildChannel.Guild.Id; + ulong? guildId = model.GuildId.ToNullable(); Data = SocketCommandBaseData.Create(client, dataModel, model.Id, guildId); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs index 98a7daefc..2167a69a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -1,3 +1,4 @@ +using Discord.Net; using System.Collections.Generic; namespace Discord.WebSocket @@ -45,13 +46,24 @@ namespace Discord.WebSocket if (socketChannel == null) { - var channelModel = guild != null - ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() - : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - - socketChannel = guild != null - ? SocketGuildChannel.Create(guild, discord.State, channelModel) - : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + try + { + var channelModel = guild != null + ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id) + .ConfigureAwait(false).GetAwaiter().GetResult() + : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false) + .GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + } + catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.MissingPermissions) + { + socketChannel = guildId != null + ? SocketGuildChannel.Create(guild, discord.State, channel.Value) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channel.Value); + } } discord.State.AddChannel(socketChannel); @@ -73,7 +85,10 @@ namespace Discord.WebSocket { foreach (var role in resolved.Roles.Value) { - var socketRole = guild.AddOrUpdateRole(role.Value); + var socketRole = guild is null + ? SocketRole.Create(null, discord.State, role.Value) + : guild.AddOrUpdateRole(role.Value); + Roles.Add(ulong.Parse(role.Key), socketRole); } } @@ -93,16 +108,19 @@ namespace Discord.WebSocket author = guild.GetUser(msg.Value.Author.Value.Id); } else - author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id); + author = (channel as SocketChannel)?.GetUser(msg.Value.Author.Value.Id); if (channel == null) { - if (!msg.Value.GuildId.IsSpecified) // assume it is a DM + if (guildId is null) // assume it is a DM { channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); + author = ((SocketDMChannel)channel).Recipient; } } + author ??= discord.State.GetOrAddUser(msg.Value.Author.Value.Id, _ => SocketGlobalUser.Create(discord, discord.State, msg.Value.Author.Value)); + var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); Messages.Add(message.Id, message); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index e5776a089..f5abb2c49 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -127,7 +127,7 @@ namespace Discord.WebSocket refMsgAuthor = guild.GetUser(refMsg.Author.Value.Id); } else - refMsgAuthor = (Channel as SocketChannel).GetUser(refMsg.Author.Value.Id); + refMsgAuthor = (Channel as SocketChannel)?.GetUser(refMsg.Author.Value.Id); if (refMsgAuthor == null) refMsgAuthor = SocketUnknownUser.Create(Discord, state, refMsg.Author.Value); } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 1e90b8f5c..b6a61cfb0 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -63,7 +63,7 @@ namespace Discord.WebSocket => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) - : base(guild.Discord, id) + : base(guild?.Discord, id) { Guild = guild; } From e551431d72838be8a514c417ea98458e6602bdfe Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:44:30 +0300 Subject: [PATCH 126/153] Max/Min length fields for ApplicationCommandOption (#2379) * implement max/min length fields for ApplicationCommandOption * fix badly formed xml comments --- .../Interactions/ApplicationCommandOption.cs | 10 ++++ .../Interactions/IApplicationCommandOption.cs | 10 ++++ .../SlashCommands/SlashCommandBuilder.cs | 51 +++++++++++++++++-- .../Attributes/MaxLengthAttribute.cs | 25 +++++++++ .../Attributes/MinLengthAttribute.cs | 25 +++++++++ .../Builders/ModuleClassBuilder.cs | 6 +++ .../SlashCommandParameterBuilder.cs | 36 +++++++++++++ .../Parameters/SlashCommandParameterInfo.cs | 12 +++++ .../Utilities/ApplicationCommandRestUtil.cs | 12 ++++- .../API/Common/ApplicationCommandOption.cs | 10 ++++ .../RestApplicationCommandOption.cs | 9 ++++ .../SocketApplicationCommandOption.cs | 9 ++++ 12 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index 5857bac81..5e4f6a81d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -81,6 +81,16 @@ namespace Discord ///
public double? MaxValue { get; set; } + /// + /// Gets or sets the minimum allowed length for a string input. + /// + public int? MinLength { get; set; } + + /// + /// Gets or sets the maximum allowed length for a string input. + /// + public int? MaxLength { get; set; } + /// /// Gets or sets the choices for string and int types for the user to pick from. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index 72554fc98..c0a752fdc 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -47,6 +47,16 @@ namespace Discord /// double? MaxValue { get; } + /// + /// Gets the minimum allowed length for a string input. + /// + int? MinLength { get; } + + /// + /// Gets the maximum allowed length for a string input. + /// + int? MaxLength { get; } + /// /// Gets the choices for string and int types for the user to pick from. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index d7d086762..bf22d4e3a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -196,7 +196,7 @@ namespace Discord /// The current builder. public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -222,6 +222,8 @@ namespace Discord ChannelTypes = channelTypes, MinValue = minValue, MaxValue = maxValue, + MinLength = minLength, + MaxLength = maxLength, }; return AddOption(option); @@ -354,6 +356,16 @@ namespace Discord /// public double? MaxValue { get; set; } + /// + /// Gets or sets the minimum allowed length for a string input. + /// + public int? MinLength { get; set; } + + /// + /// Gets or sets the maximum allowed length for a string input. + /// + public int? MaxLength { get; set; } + /// /// Gets or sets the choices for string and int types for the user to pick from. /// @@ -377,6 +389,7 @@ namespace Discord { bool isSubType = Type == ApplicationCommandOptionType.SubCommandGroup; bool isIntType = Type == ApplicationCommandOptionType.Integer; + bool isStrType = Type == ApplicationCommandOptionType.String; if (isSubType && (Options == null || !Options.Any())) throw new InvalidOperationException("SubCommands/SubCommandGroups must have at least one option"); @@ -390,6 +403,12 @@ namespace Discord if (isIntType && MaxValue != null && MaxValue % 1 != 0) throw new InvalidOperationException("MaxValue cannot have decimals on Integer command options."); + if(isStrType && MinLength is not null && MinLength < 0) + throw new InvalidOperationException("MinLength cannot be smaller than 0."); + + if (isStrType && MaxLength is not null && MaxLength < 1) + throw new InvalidOperationException("MaxLength cannot be smaller than 1."); + return new ApplicationCommandOptionProperties { Name = Name, @@ -404,7 +423,9 @@ namespace Discord IsAutocomplete = IsAutocomplete, ChannelTypes = ChannelTypes, MinValue = MinValue, - MaxValue = MaxValue + MaxValue = MaxValue, + MinLength = MinLength, + MaxLength = MaxLength, }; } @@ -425,7 +446,7 @@ namespace Discord /// The current builder. public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -447,6 +468,8 @@ namespace Discord IsAutocomplete = isAutocomplete, MinValue = minValue, MaxValue = maxValue, + MinLength = minLength, + MaxLength = maxLength, Options = options, Type = type, Choices = (choices ?? Array.Empty()).ToList(), @@ -669,6 +692,28 @@ namespace Discord return this; } + /// + /// Sets the current builders min length field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithMinLength(int length) + { + MinLength = length; + return this; + } + + /// + /// Sets the current builders max length field. + /// + /// The value to set. + /// The current builder. + public SlashCommandOptionBuilder WithMaxLength(int lenght) + { + MaxLength = lenght; + return this; + } + /// /// Sets the current type of this builder. /// diff --git a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs new file mode 100644 index 000000000..1099e7d92 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the maximum length allowed for a string type parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class MaxLengthAttribute : Attribute + { + /// + /// Gets the maximum length allowed for a string type parameter. + /// + public int Length { get; } + + /// + /// Sets the maximum length allowed for a string type parameter. + /// + /// Maximum string length allowed. + public MaxLengthAttribute(int lenght) + { + Length = lenght; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs new file mode 100644 index 000000000..7d0b0fd63 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the minimum length allowed for a string type parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class MinLengthAttribute : Attribute + { + /// + /// Gets the minimum length allowed for a string type parameter. + /// + public int Length { get; } + + /// + /// Sets the minimum length allowed for a string type parameter. + /// + /// Minimum string length allowed. + public MinLengthAttribute(int lenght) + { + Length = lenght; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 1bbdfcc4a..35126a674 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -463,6 +463,12 @@ namespace Discord.Interactions.Builders case MinValueAttribute minValue: builder.MinValue = minValue.Value; break; + case MinLengthAttribute minLength: + builder.MinLength = minLength.Length; + break; + case MaxLengthAttribute maxLength: + builder.MaxLength = maxLength.Length; + break; case ComplexParameterAttribute complexParameter: { builder.IsComplexParameter = true; diff --git a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs index d600c9cc7..6f8038cef 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs @@ -28,6 +28,16 @@ namespace Discord.Interactions.Builders /// public double? MinValue { get; set; } + /// + /// Gets or sets the minimum length allowed for a string type parameter. + /// + public int? MinLength { get; set; } + + /// + /// Gets or sets the maximum length allowed for a string type parameter. + /// + public int? MaxLength { get; set; } + /// /// Gets a collection of the choices of this command. /// @@ -125,6 +135,32 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandParameterBuilder WithMinLength(int length) + { + MinLength = length; + return this; + } + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandParameterBuilder WithMaxLength(int length) + { + MaxLength = length; + return this; + } + /// /// Adds parameter choices to . /// diff --git a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs index 8702d69f7..0bce42186 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs @@ -38,6 +38,16 @@ namespace Discord.Interactions /// public double? MaxValue { get; } + /// + /// Gets the minimum length allowed for a string type parameter. + /// + public int? MinLength { get; } + + /// + /// Gets the maximum length allowed for a string type parameter. + /// + public int? MaxLength { get; } + /// /// Gets the that will be used to convert the incoming into /// . @@ -86,6 +96,8 @@ namespace Discord.Interactions Description = builder.Description; MaxValue = builder.MaxValue; MinValue = builder.MinValue; + MinLength = builder.MinLength; + MaxLength = builder.MaxLength; IsComplexParameter = builder.IsComplexParameter; IsAutocomplete = builder.Autocomplete; Choices = builder.Choices.ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index e4b6f893c..409c0e796 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -23,7 +23,9 @@ namespace Discord.Interactions ChannelTypes = parameterInfo.ChannelTypes?.ToList(), IsAutocomplete = parameterInfo.IsAutocomplete, MaxValue = parameterInfo.MaxValue, - MinValue = parameterInfo.MinValue + MinValue = parameterInfo.MinValue, + MinLength = parameterInfo.MinLength, + MaxLength = parameterInfo.MaxLength, }; parameterInfo.TypeConverter.Write(props, parameterInfo); @@ -209,7 +211,13 @@ namespace Discord.Interactions Name = x.Name, Value = x.Value }).ToList(), - Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList() + Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), + MaxLength = commandOption.MaxLength, + MinLength = commandOption.MinLength, + MaxValue = commandOption.MaxValue, + MinValue = commandOption.MinValue, + IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), + ChannelTypes = commandOption.ChannelTypes.ToList(), }; public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index d703bd46b..fff5730f4 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -38,6 +38,12 @@ namespace Discord.API [JsonProperty("channel_types")] public Optional ChannelTypes { get; set; } + [JsonProperty("min_length")] + public Optional MinLength { get; set; } + + [JsonProperty("max_length")] + public Optional MaxLength { get; set; } + public ApplicationCommandOption() { } public ApplicationCommandOption(IApplicationCommandOption cmd) @@ -56,6 +62,8 @@ namespace Discord.API Default = cmd.IsDefault ?? Optional.Unspecified; MinValue = cmd.MinValue ?? Optional.Unspecified; MaxValue = cmd.MaxValue ?? Optional.Unspecified; + MinLength = cmd.MinLength ?? Optional.Unspecified; + MaxLength = cmd.MaxLength ?? Optional.Unspecified; Autocomplete = cmd.IsAutocomplete ?? Optional.Unspecified; Name = cmd.Name; @@ -77,6 +85,8 @@ namespace Discord.API Default = option.IsDefault ?? Optional.Unspecified; MinValue = option.MinValue ?? Optional.Unspecified; MaxValue = option.MaxValue ?? Optional.Unspecified; + MinLength = option.MinLength ?? Optional.Unspecified; + MaxLength = option.MaxLength ?? Optional.Unspecified; ChannelTypes = option.ChannelTypes?.ToArray() ?? Optional.Unspecified; diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index 86c6019ed..c47080be7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -35,6 +35,12 @@ namespace Discord.Rest /// public double? MaxValue { get; private set; } + /// + public int? MinLength { get; private set; } + + /// + public int? MaxLength { get; private set; } + /// /// Gets a collection of s for this command. /// @@ -78,6 +84,9 @@ namespace Discord.Rest if (model.Autocomplete.IsSpecified) IsAutocomplete = model.Autocomplete.Value; + MinLength = model.MinLength.ToNullable(); + MaxLength = model.MaxLength.ToNullable(); + Options = model.Options.IsSpecified ? model.Options.Value.Select(Create).ToImmutableArray() : ImmutableArray.Create(); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs index 27777749a..478c7cb54 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs @@ -33,6 +33,12 @@ namespace Discord.WebSocket /// public double? MaxValue { get; private set; } + /// + public int? MinLength { get; private set; } + + /// + public int? MaxLength { get; private set; } + /// /// Gets a collection of choices for the user to pick from. /// @@ -72,6 +78,9 @@ namespace Discord.WebSocket IsAutocomplete = model.Autocomplete.ToNullable(); + MinLength = model.MinLength.ToNullable(); + MaxLength = model.MaxLength.ToNullable(); + Choices = model.Choices.IsSpecified ? model.Choices.Value.Select(SocketApplicationCommandChoice.Create).ToImmutableArray() : ImmutableArray.Create(); From 02bc3b797745784b41e00789b6da209f3960635f Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:05:22 +0200 Subject: [PATCH 127/153] Fix NRE on commandbase data assignment (#2414) --- .../Entities/Interactions/CommandBase/RestCommandBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs index 22e56a733..102ede7b7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs @@ -49,6 +49,9 @@ namespace Discord.Rest internal override async Task UpdateAsync(DiscordRestClient client, Model model, bool doApiCall) { await base.UpdateAsync(client, model, doApiCall).ConfigureAwait(false); + + if (model.Data.IsSpecified && model.Data.Value is RestCommandBaseData data) + Data = data; } /// From 500e7b44caaf9bd47bbbed58a19fbe23dbd8ab64 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 10 Aug 2022 11:37:40 +0300 Subject: [PATCH 128/153] Using RespondWithModalAsync() without prior IModal declaration (#2369) * add RespondWithModalAsync method for initializing missing ModalInfos on runtime * update method name and add inline docs --- .../IDiscordInteractionExtensions.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 8f0987661..d970b9930 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -19,9 +19,36 @@ namespace Discord.Interactions if (!ModalUtils.TryGet(out var modalInfo)) throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}"); + await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); + } + + /// + /// Respond to an interaction with a . + /// + /// + /// This method overload uses the parameter to create a new + /// if there isn't a built one already in cache. + /// + /// Type of the implementation. + /// The interaction to respond to. + /// Interaction service instance that should be used to build s. + /// The request options for this request. + /// Delegate that can be used to modify the modal. + /// + public static async Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService, + RequestOptions options = null, Action modifyModal = null) + where T : class, IModal + { + var modalInfo = ModalUtils.GetOrAdd(interactionService); + + await SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal); + } + + private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action modifyModal = null) + { var builder = new ModalBuilder(modalInfo.Title, customId); - foreach(var input in modalInfo.Components) + foreach (var input in modalInfo.Components) switch (input) { case TextInputComponentInfo textComponent: From 8dfe19f32892e60ae259a507e4596b76b1b5003f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gutyina=20Gerg=C5=91?= Date: Mon, 15 Aug 2022 19:33:23 +0200 Subject: [PATCH 129/153] Fix placeholder length being hardcoded (#2421) * Fix placeholder length being hardcoded * Add docs for TextInputBuilder.MaxPlaceholderLength --- .../Interactions/MessageComponents/ComponentBuilder.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 37342b039..fd8798ed3 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1198,6 +1198,10 @@ namespace Discord public class TextInputBuilder { + /// + /// The max length of a . + /// + public const int MaxPlaceholderLength = 100; public const int LargestMaxLength = 4000; /// @@ -1229,13 +1233,13 @@ namespace Discord /// /// Gets or sets the placeholder of the current text input. /// - /// is longer than 100 characters + /// is longer than characters public string Placeholder { get => _placeholder; - set => _placeholder = (value?.Length ?? 0) <= 100 + set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength ? value - : throw new ArgumentException("Placeholder cannot have more than 100 characters."); + : throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters."); } /// From 65b98f8b1251f1cfa64a63c54809a71e4111ad3d Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 16 Aug 2022 03:34:17 +1000 Subject: [PATCH 130/153] Update xmldocs to reflect the ConnectedUsers split (#2418) --- .../Entities/Channels/SocketGuildChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 16ed7b32d..808982785 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -36,8 +36,8 @@ namespace Discord.WebSocket /// Gets a collection of users that are able to view the channel. /// /// - /// If this channel is a voice channel, a collection of users who are currently connected to this channel - /// is returned. + /// If this channel is a voice channel, use to retrieve a + /// collection of users who are currently connected to this channel. /// /// /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). From 6da595e07467c91db3e81e66f7e7bd2b2b9fc7c1 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:52:57 +0300 Subject: [PATCH 131/153] fix ci/cd error (#2428) --- src/Discord.Net.Core/Discord.Net.Core.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 41d83bbc8..005280c4d 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -16,7 +16,6 @@ all - @@ -27,4 +26,7 @@ + + + \ No newline at end of file From b6b5e95f48af647531292f3c3c3c53af8f98ef7a Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:53:14 +0200 Subject: [PATCH 132/153] Fix role icon & emoji assignment. (#2416) --- src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 3b2946a0d..2f6d1f062 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -23,7 +23,7 @@ namespace Discord.Rest { role.Guild.Features.EnsureFeature(GuildFeature.RoleIcons); - if (args.Icon.IsSpecified && args.Emoji.IsSpecified) + if ((args.Icon.IsSpecified && args.Icon.Value != null) && (args.Emoji.IsSpecified && args.Emoji.Value != null)) { throw new ArgumentException("Emoji and Icon properties cannot be present on a role at the same time."); } @@ -36,18 +36,18 @@ namespace Discord.Rest Mentionable = args.Mentionable, Name = args.Name, Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create(), - Icon = args.Icon.IsSpecified ? args.Icon.Value.Value.ToModel() : Optional.Unspecified, - Emoji = args.Emoji.GetValueOrDefault()?.Name ?? Optional.Unspecified + Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() ?? null : Optional.Unspecified, + Emoji = args.Emoji.IsSpecified ? args.Emoji.Value?.Name ?? "" : Optional.Create(), }; - if (args.Icon.IsSpecified && role.Emoji != null) + if ((args.Icon.IsSpecified && args.Icon.Value != null) && role.Emoji != null) { - apiArgs.Emoji = null; + apiArgs.Emoji = ""; } - if (args.Emoji.IsSpecified && !string.IsNullOrEmpty(role.Icon)) + if ((args.Emoji.IsSpecified && args.Emoji.Value != null) && !string.IsNullOrEmpty(role.Icon)) { - apiArgs.Icon = null; + apiArgs.Icon = Optional.Unspecified; } var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); From b7b7964de97b656579845435c6050104d2c9daf8 Mon Sep 17 00:00:00 2001 From: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Date: Sun, 21 Aug 2022 16:54:19 +0500 Subject: [PATCH 133/153] Fix IGuild.GetBansAsync() (#2424) fix the problem of not being able to get more than 1000 bans --- src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 20140994f..8195a2cea 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -180,7 +180,7 @@ namespace Discord.Rest }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + if (lastPage.Count != DiscordConfig.MaxBansPerBatch) return false; if (dir == Direction.Before) info.Position = lastPage.Min(x => x.User.Id); From 917118d094eb1969c7da6ff8c6e4583c450604f6 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:56:02 +0300 Subject: [PATCH 134/153] [DOCS] Add a note about `DontAutoRegisterAttribute` (#2430) * add a note about `DontAutoRegisterAttribute` * Remove "to to" and add punctuation Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 5cf38bff1..37c579159 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -294,7 +294,7 @@ By nesting commands inside a module that is tagged with [GroupAttribute] you can > [!NOTE] > To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute] > -> However, you have to be careful to prevent overlapping ids of buttons and modals +> However, you have to be careful to prevent overlapping ids of buttons and modals. [!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] @@ -346,10 +346,13 @@ Command registration methods can only be used after the gateway client is ready Methods like `AddModulesToGuildAsync()`, `AddCommandsToGuildAsync()`, `AddModulesGloballyAsync()` and `AddCommandsGloballyAsync()` can be used to register cherry picked modules or commands to global/guild scopes. +> [!NOTE] +> [DontAutoRegisterAttribute] can be used on module classes to prevent `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` from registering them to the Discord. + > [!NOTE] > In debug environment, since Global commands can take up to 1 hour to register/update, > it is adviced to register your commands to a test guild for your changes to take effect immediately. -> You can use preprocessor directives to create a simple logic for registering commands as seen above +> You can use preprocessor directives to create a simple logic for registering commands as seen above. ## Interaction Utility @@ -377,6 +380,7 @@ delegate can be used to create HTTP responses from a deserialized json object st [DependencyInjection]: xref:Guides.DI.Intro [GroupAttribute]: xref:Discord.Interactions.GroupAttribute +[DontAutoRegisterAttribute]: xref:Discord.Interactions.DontAutoRegisterAttribute [InteractionService]: xref:Discord.Interactions.InteractionService [InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig [InteractionModuleBase]: xref:Discord.Interactions.InteractionModuleBase From 92215b1f746cdeda4fe53aa0568d078defc5afc9 Mon Sep 17 00:00:00 2001 From: Ge Date: Sun, 21 Aug 2022 19:57:00 +0800 Subject: [PATCH 135/153] fix: Missing Fact attribute in ColorTests (#2425) --- test/Discord.Net.Tests.Unit/ColorTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Discord.Net.Tests.Unit/ColorTests.cs b/test/Discord.Net.Tests.Unit/ColorTests.cs index 46d8feabb..48a6041e5 100644 --- a/test/Discord.Net.Tests.Unit/ColorTests.cs +++ b/test/Discord.Net.Tests.Unit/ColorTests.cs @@ -10,6 +10,7 @@ namespace Discord /// public class ColorTests { + [Fact] public void Color_New() { Assert.Equal(0u, new Color().RawValue); From 89a8ea161fcc7540572e7d272dff8e2069b06195 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:57:51 +0300 Subject: [PATCH 136/153] feat: Embed comparison (#2347) --- .../Entities/Messages/Embed.cs | 39 +++++ .../Entities/Messages/EmbedAuthor.cs | 31 ++++ .../Entities/Messages/EmbedBuilder.cs | 141 ++++++++++++++++++ .../Entities/Messages/EmbedField.cs | 31 ++++ .../Entities/Messages/EmbedFooter.cs | 31 ++++ .../Entities/Messages/EmbedImage.cs | 31 ++++ .../Entities/Messages/EmbedProvider.cs | 31 ++++ .../Entities/Messages/EmbedThumbnail.cs | 31 ++++ .../Entities/Messages/EmbedVideo.cs | 31 ++++ 9 files changed, 397 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 7fa6f6f36..c1478f56c 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -94,5 +94,44 @@ namespace Discord /// public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; + + public static bool operator ==(Embed left, Embed right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(Embed left, Embed right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is Embed embed && Equals(embed); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(Embed embed) + => GetHashCode() == embed?.GetHashCode(); + + /// + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = hash * 23 + (Type, Title, Description, Timestamp, Color, Image, Video, Author, Footer, Provider, Thumbnail).GetHashCode(); + foreach(var field in Fields) + hash = hash * 23 + field.GetHashCode(); + return hash; + } + } } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index 3b11f6a8b..fdd51e6c9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -41,5 +42,35 @@ namespace Discord /// /// public override string ToString() => Name; + + public static bool operator ==(EmbedAuthor? left, EmbedAuthor? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedAuthor? left, EmbedAuthor? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedAuthor embedAuthor && Equals(embedAuthor); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedAuthor? embedAuthor) + => GetHashCode() == embedAuthor?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Url, IconUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 1e2a7b0d7..db38b9fb7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -481,6 +481,55 @@ namespace Discord return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); } + + public static bool operator ==(EmbedBuilder left, EmbedBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedBuilder left, EmbedBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedBuilder embedBuilder && Equals(embedBuilder); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedBuilder embedBuilder) + { + if (embedBuilder is null) + return false; + + if (Fields.Count != embedBuilder.Fields.Count) + return false; + + for (var i = 0; i < _fields.Count; i++) + if (_fields[i] != embedBuilder._fields[i]) + return false; + + return _title == embedBuilder?._title + && _description == embedBuilder?._description + && _image == embedBuilder?._image + && _thumbnail == embedBuilder?._thumbnail + && Timestamp == embedBuilder?.Timestamp + && Color == embedBuilder?.Color + && Author == embedBuilder?.Author + && Footer == embedBuilder?.Footer + && Url == embedBuilder?.Url; + } + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -597,6 +646,37 @@ namespace Discord /// public EmbedField Build() => new EmbedField(Name, Value.ToString(), IsInline); + + public static bool operator ==(EmbedFieldBuilder left, EmbedFieldBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFieldBuilder left, EmbedFieldBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedFieldBuilder embedFieldBuilder && Equals(embedFieldBuilder); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedFieldBuilder embedFieldBuilder) + => _name == embedFieldBuilder?._name + && _value == embedFieldBuilder?._value + && IsInline == embedFieldBuilder?.IsInline; + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -697,6 +777,37 @@ namespace Discord /// public EmbedAuthor Build() => new EmbedAuthor(Name, Url, IconUrl, null); + + public static bool operator ==(EmbedAuthorBuilder left, EmbedAuthorBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedAuthorBuilder left, EmbedAuthorBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedAuthorBuilder embedAuthorBuilder && Equals(embedAuthorBuilder); + + /// + /// Determines whether the specified is equals to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedAuthorBuilder embedAuthorBuilder) + => _name == embedAuthorBuilder?._name + && Url == embedAuthorBuilder?.Url + && IconUrl == embedAuthorBuilder?.IconUrl; + + /// + public override int GetHashCode() => base.GetHashCode(); } /// @@ -777,5 +888,35 @@ namespace Discord /// public EmbedFooter Build() => new EmbedFooter(Text, IconUrl, null); + + public static bool operator ==(EmbedFooterBuilder left, EmbedFooterBuilder right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFooterBuilder left, EmbedFooterBuilder right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedFooterBuilder embedFooterBuilder && Equals(embedFooterBuilder); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedFooterBuilder embedFooterBuilder) + => _text == embedFooterBuilder?._text + && IconUrl == embedFooterBuilder?.IconUrl; + + /// + public override int GetHashCode() => base.GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index f6aa2af3b..1196869fe 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -36,5 +37,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Name; + + public static bool operator ==(EmbedField? left, EmbedField? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedField? left, EmbedField? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current object + /// + public override bool Equals(object obj) + => obj is EmbedField embedField && Equals(embedField); + + /// + /// Determines whether the specified is equal to the current + /// + /// + /// + public bool Equals(EmbedField? embedField) + => GetHashCode() == embedField?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Value, Inline).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 4c507d017..5a1f13158 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -43,5 +44,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Text; + + public static bool operator ==(EmbedFooter? left, EmbedFooter? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedFooter? left, EmbedFooter? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedFooter embedFooter && Equals(embedFooter); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedFooter? embedFooter) + => GetHashCode() == embedFooter?.GetHashCode(); + + /// + public override int GetHashCode() + => (Text, IconUrl, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index 9ce2bfe73..85a638dc8 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -53,5 +54,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedImage? left, EmbedImage? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedImage? left, EmbedImage? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedImage embedImage && Equals(embedImage); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedImage? embedImage) + => GetHashCode() == embedImage?.GetHashCode(); + + /// + public override int GetHashCode() + => (Height, Width, Url, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 960fb3d78..f2ee74613 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -35,5 +36,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Name; + + public static bool operator ==(EmbedProvider? left, EmbedProvider? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedProvider? left, EmbedProvider? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedProvider embedProvider && Equals(embedProvider); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedProvider? embedProvider) + => GetHashCode() == embedProvider?.GetHashCode(); + + /// + public override int GetHashCode() + => (Name, Url).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 7f7b582dc..65c8139c3 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -53,5 +54,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedThumbnail? left, EmbedThumbnail? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedThumbnail? left, EmbedThumbnail? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedThumbnail embedThumbnail && Equals(embedThumbnail); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedThumbnail? embedThumbnail) + => GetHashCode() == embedThumbnail?.GetHashCode(); + + /// + public override int GetHashCode() + => (Width, Height, Url, ProxyUrl).GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index ca0300e80..0762ed8e7 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Discord @@ -47,5 +48,35 @@ namespace Discord /// A string that resolves to . /// public override string ToString() => Url; + + public static bool operator ==(EmbedVideo? left, EmbedVideo? right) + => left is null ? right is null + : left.Equals(right); + + public static bool operator !=(EmbedVideo? left, EmbedVideo? right) + => !(left == right); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// + /// If the object passes is an , will be called to compare the 2 instances + /// + /// The object to compare with the current + /// + public override bool Equals(object obj) + => obj is EmbedVideo embedVideo && Equals(embedVideo); + + /// + /// Determines whether the specified is equal to the current + /// + /// The to compare with the current + /// + public bool Equals(EmbedVideo? embedVideo) + => GetHashCode() == embedVideo?.GetHashCode(); + + /// + public override int GetHashCode() + => (Width, Height, Url).GetHashCode(); } } From ddcf68a29fce08bd59be9e39c5732e4eb1d3a1b2 Mon Sep 17 00:00:00 2001 From: Charlie U <52503242+cpurules@users.noreply.github.com> Date: Sun, 21 Aug 2022 07:58:51 -0400 Subject: [PATCH 137/153] Fix broken code snippet in dependency injection docs (#2420) * Fixed markdown formatting to show code snippet * Fixed constructor injection code snippet pointer --- docs/guides/dependency_injection/injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/dependency_injection/injection.md b/docs/guides/dependency_injection/injection.md index c7d40c479..85a77476f 100644 --- a/docs/guides/dependency_injection/injection.md +++ b/docs/guides/dependency_injection/injection.md @@ -16,7 +16,7 @@ This can be done through property or constructor. Services can be injected from the constructor of the class. This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class. -[!code-csharp[Property Injection(samples/property-injecting.cs)]] +[!code-csharp[Constructor Injection](samples/ctor-injecting.cs)] ## Injecting through properties From 32b03c8063332d50c93bbf0eefa55044853b6ce1 Mon Sep 17 00:00:00 2001 From: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> Date: Sun, 21 Aug 2022 16:14:55 +0200 Subject: [PATCH 138/153] Added support for lottie stickers (#2359) --- .../API/Rest/CreateStickerParams.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs index b330a0111..a0871bc64 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateStickerParams.cs @@ -1,4 +1,5 @@ using Discord.Net.Rest; + using System.Collections.Generic; using System.IO; namespace Discord.API.Rest @@ -20,14 +21,21 @@ namespace Discord.API.Rest ["tags"] = Tags }; - string contentType = "image/png"; - + string contentType; if (File is FileStream fileStream) - contentType = $"image/{Path.GetExtension(fileStream.Name)}"; + { + var extension = Path.GetExtension(fileStream.Name).TrimStart('.'); + contentType = extension == "json" ? "application/json" : $"image/{extension}"; + } else if (FileName != null) - contentType = $"image/{Path.GetExtension(FileName)}"; + { + var extension = Path.GetExtension(FileName).TrimStart('.'); + contentType = extension == "json" ? "application/json" : $"image/{extension}"; + } + else + contentType = "image/png"; - d["file"] = new MultipartFile(File, FileName ?? "image", contentType.Replace(".", "")); + d["file"] = new MultipartFile(File, FileName ?? "image", contentType); return d; } From 39bbd298c37a7e2766f9eacb65e768930dee67a9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:45:27 +0300 Subject: [PATCH 139/153] Interactions Command Localization (#2395) * Request headers (#2394) * add support for per-request headers * remove unnecessary usings * Revert "remove unnecessary usings" This reverts commit 8d674fe4faf985b117f143fae3877a1698170ad2. * remove nullable strings from RequestOptions * Add Localization Support to Interaction Service (#2211) * add json and resx localization managers * add utils class for getting command paths * update json regex to make langage code optional * remove IServiceProvider from ILocalizationManager method params * replace the command path method in command map * add localization fields to rest and websocket application command entity implementations * move deconstruct extensions method to extensions folder * add withLocalizations parameter to rest methods * fix build error * add rest conversions to interaction service * add localization to the rest methods * add inline docs * fix implementation bugs * add missing inline docs * inline docs correction (Name/Description Localized properties) * add choice localization * fix conflicts * fix conflicts * add missing command props fields to ToApplicationCommandProps methods * add locale parameter to Get*ApplicationCommandsAsync methods for fetching localized command names/descriptions * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> * add inline docs to LocalizationTarget * fix upstream merge errors * fix command parsing for context command names with space char * fix command parsing for context command names with space char * fix failed to generate buket id * fix get guild commands endpoint * update rexs localization manager to use single-file pattern * Upstream Merge Localization Branch (#2434) * fix ci/cd error (#2428) * Fix role icon & emoji assignment. (#2416) * Fix IGuild.GetBansAsync() (#2424) fix the problem of not being able to get more than 1000 bans * [DOCS] Add a note about `DontAutoRegisterAttribute` (#2430) * add a note about `DontAutoRegisterAttribute` * Remove "to to" and add punctuation Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> * fix: Missing Fact attribute in ColorTests (#2425) * feat: Embed comparison (#2347) * Fix broken code snippet in dependency injection docs (#2420) * Fixed markdown formatting to show code snippet * Fixed constructor injection code snippet pointer * Added support for lottie stickers (#2359) Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> * remove unnecassary fields from ResxLocalizationManager * update int framework guides * remove space character tokenization from ResxLocalizationManager Co-authored-by: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Co-authored-by: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Co-authored-by: Misha133 <61027276+Misha-133@users.noreply.github.com> Co-authored-by: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Co-authored-by: Ge Co-authored-by: Charlie U <52503242+cpurules@users.noreply.github.com> Co-authored-by: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 41 +++ .../Entities/Guilds/IGuild.cs | 7 +- .../Interactions/ApplicationCommandOption.cs | 92 ++++- .../ApplicationCommandOptionChoice.cs | 33 ++ .../ApplicationCommandProperties.cs | 52 +++ .../ContextMenus/MessageCommandBuilder.cs | 70 ++++ .../ContextMenus/UserCommandBuilder.cs | 72 +++- .../Interactions/IApplicationCommand.cs | 26 ++ .../Interactions/IApplicationCommandOption.cs | 26 ++ .../IApplicationCommandOptionChoice.cs | 15 + .../SlashCommands/SlashCommandBuilder.cs | 325 ++++++++++++++++-- .../Extensions/GenericCollectionExtensions.cs | 15 + src/Discord.Net.Core/IDiscordClient.cs | 4 +- src/Discord.Net.Core/Net/Rest/IRestClient.cs | 10 +- src/Discord.Net.Core/RequestOptions.cs | 12 +- src/Discord.Net.Core/Utils/Preconditions.cs | 10 +- .../InteractionService.cs | 6 + .../InteractionServiceConfig.cs | 5 + .../ILocalizationManager.cs | 32 ++ .../JsonLocalizationManager.cs | 72 ++++ .../ResxLocalizationManager.cs | 55 +++ .../LocalizationTarget.cs | 25 ++ .../Map/CommandMap.cs | 23 +- .../Utilities/ApplicationCommandRestUtil.cs | 88 ++++- .../Utilities/CommandHierarchy.cs | 53 +++ .../API/Common/ApplicationCommand.cs | 13 + .../API/Common/ApplicationCommandOption.cs | 21 ++ .../Common/ApplicationCommandOptionChoice.cs | 7 + .../Rest/CreateApplicationCommandParams.cs | 15 +- .../Rest/ModifyApplicationCommandParams.cs | 7 + src/Discord.Net.Rest/BaseDiscordClient.cs | 2 +- src/Discord.Net.Rest/ClientHelper.cs | 12 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 31 +- src/Discord.Net.Rest/DiscordRestClient.cs | 14 +- .../Entities/Guilds/GuildHelper.cs | 6 +- .../Entities/Guilds/RestGuild.cs | 16 +- .../Interactions/InteractionHelper.cs | 20 +- .../Interactions/RestApplicationCommand.cs | 35 ++ .../RestApplicationCommandChoice.cs | 17 + .../RestApplicationCommandOption.cs | 37 +- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 20 +- .../Net/Queue/Requests/RestRequest.cs | 5 +- .../DiscordSocketClient.cs | 10 +- .../Entities/Guilds/SocketGuild.cs | 11 +- .../SocketApplicationCommand.cs | 35 ++ .../SocketApplicationCommandChoice.cs | 17 + .../SocketApplicationCommandOption.cs | 35 ++ 47 files changed, 1403 insertions(+), 152 deletions(-) create mode 100644 src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs create mode 100644 src/Discord.Net.Interactions/LocalizationTarget.cs create mode 100644 src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 37c579159..21ea365de 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -376,6 +376,47 @@ respond to the Interactions within your command modules you need to perform the delegate can be used to create HTTP responses from a deserialized json object string. - Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). +## Localization + +Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`. + +### ResXLocalizationManager + +`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates. + +### JsonLocalizationManager + +`JsonLocaliationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to: + +```json +{ + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + } + }, + "group_1":{ + "name": "localized_name", + "description": "localized_description", + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + }, + "parameter_2":{ + "name": "localized_name", + "description": "localized_description" + } + } + } +} +``` + [AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion [DependencyInjection]: xref:Guides.DI.Intro diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 775ff9e65..34a08f1e7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1194,12 +1194,17 @@ namespace Discord /// /// Gets this guilds application commands. /// + /// + /// Whether to include full localization dictionaries in the returned objects, + /// instead of the localized name and description fields. + /// + /// The target locale of the localized name and description fields. Sets the X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - Task> GetApplicationCommandsAsync(RequestOptions options = null); + Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Gets an application command within this guild with the specified id. diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index 5e4f6a81d..bceefda32 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -12,6 +13,8 @@ namespace Discord { private string _name; private string _description; + private IDictionary _nameLocalizations = new Dictionary(); + private IDictionary _descriptionLocalizations = new Dictionary(); /// /// Gets or sets the name of this option. @@ -21,18 +24,7 @@ namespace Discord get => _name; set { - if (value == null) - throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null."); - - if (value.Length > 32) - throw new ArgumentOutOfRangeException(nameof(value), "Name length must be less than or equal to 32."); - - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new FormatException($"{nameof(value)} must match the regex ^[\\w-]{{1,32}}$"); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidOptionName(value); _name = value; } } @@ -43,12 +35,11 @@ namespace Discord public string Description { get => _description; - set => _description = value?.Length switch + set { - > 100 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be less than or equal to 100."), - 0 => throw new ArgumentOutOfRangeException(nameof(value), "Description length must be at least 1."), - _ => value - }; + EnsureValidOptionDescription(value); + _description = value; + } } /// @@ -105,5 +96,72 @@ namespace Discord /// Gets or sets the allowed channel types for this option. /// public List ChannelTypes { get; set; } + + /// + /// Gets or sets the localization dictionary for the name field of this option. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionName(name); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this option. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary DescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidOptionDescription(description); + } + _descriptionLocalizations = value; + } + } + + private static void EnsureValidOptionName(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name), $"{nameof(Name)} cannot be null."); + + if (name.Length > 32) + throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32."); + + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$"); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + private static void EnsureValidOptionDescription(string description) + { + switch (description.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(description), + "Description length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(description), "Description length must at least 1."); + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs index 6a908b075..8f1ecc6d2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -1,4 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace Discord { @@ -9,6 +13,7 @@ namespace Discord { private string _name; private object _value; + private IDictionary _nameLocalizations = new Dictionary(); /// /// Gets or sets the name of this choice. @@ -40,5 +45,33 @@ namespace Discord _value = value; } } + + /// + /// Gets or sets the localization dictionary for the name field of this choice. + /// + /// Thrown when any of the dictionary keys is an invalid locale. + public IDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException("Key values of the dictionary must be valid language codes."); + + switch (name.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(value), + "Name length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); + } + } + + _nameLocalizations = value; + } + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 9b3ac8453..7ca16a27d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -1,3 +1,10 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -5,6 +12,9 @@ namespace Discord /// public abstract class ApplicationCommandProperties { + private IReadOnlyDictionary _nameLocalizations; + private IReadOnlyDictionary _descriptionLocalizations; + internal abstract ApplicationCommandType Type { get; } /// @@ -17,6 +27,48 @@ namespace Discord /// public Optional IsDefaultPermission { get; set; } + /// + /// Gets or sets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations + { + get => _nameLocalizations; + set + { + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + _nameLocalizations = value; + } + } + + /// + /// Gets or sets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations + { + get => _descriptionLocalizations; + set + { + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } + _descriptionLocalizations = value; + } + } + /// /// Gets or sets whether or not this command can be used in DMs. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index 59040dd4e..ed49c685d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -31,6 +36,11 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary _nameLocalizations; /// /// Build the current builder into a class. @@ -86,6 +97,30 @@ namespace Discord return this; } + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public MessageCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -97,6 +132,41 @@ namespace Discord return this; } + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public MessageCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + /// /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index 7c82dce55..d8bb2e056 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + namespace Discord { /// @@ -5,7 +10,7 @@ namespace Discord /// public class UserCommandBuilder { - /// + /// /// Returns the maximum length a commands name allowed by Discord. /// public const int MaxNameLength = 32; @@ -31,6 +36,11 @@ namespace Discord /// public bool IsDefaultPermission { get; set; } = true; + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + /// /// Gets or sets whether or not this command can be used in DMs. /// @@ -42,6 +52,7 @@ namespace Discord public GuildPermission? DefaultMemberPermissions { get; set; } private string _name; + private Dictionary _nameLocalizations; /// /// Build the current builder into a class. @@ -84,6 +95,30 @@ namespace Discord return this; } + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public UserCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + /// /// Sets whether or not this command can be used in dms /// @@ -95,6 +130,41 @@ namespace Discord return this; } + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public UserCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + private static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + /// /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 58a002649..6f9ce7a45 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -52,6 +52,32 @@ namespace Discord /// IReadOnlyCollection Options { get; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + IReadOnlyDictionary DescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string DescriptionLocalized { get; } + /// /// Modifies the current application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs index c0a752fdc..fb179b661 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs @@ -71,5 +71,31 @@ namespace Discord /// Gets the allowed channel types for this option. /// IReadOnlyCollection ChannelTypes { get; } + + /// + /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + IReadOnlyDictionary DescriptionLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to true when requesting the command. + /// + string DescriptionLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs index 631706c6f..3f76bae72 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Discord { /// @@ -14,5 +16,18 @@ namespace Discord /// Gets the value of the choice. /// object Value { get; } + + /// + /// Gets the localization dictionary for the name field of this command option. + /// + IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + string NameLocalized { get; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index bf22d4e3a..579289304 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Net.Sockets; using System.Text.RegularExpressions; namespace Discord @@ -31,18 +34,7 @@ namespace Discord get => _name; set { - Preconditions.NotNullOrEmpty(value, nameof(value)); - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, MaxNameLength, nameof(value)); - - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(value)); - - if (value.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); - + EnsureValidCommandName(value); _name = value; } } @@ -55,10 +47,7 @@ namespace Discord get => _description; set { - Preconditions.NotNullOrEmpty(value, nameof(Description)); - Preconditions.AtLeast(value.Length, 1, nameof(Description)); - Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description)); - + EnsureValidCommandDescription(value); _description = value; } } @@ -76,6 +65,16 @@ namespace Discord } } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations; + /// /// Gets or sets whether the command is enabled by default when the app is added to a guild /// @@ -93,6 +92,8 @@ namespace Discord private string _name; private string _description; + private Dictionary _nameLocalizations; + private Dictionary _descriptionLocalizations; private List _options; /// @@ -106,6 +107,8 @@ namespace Discord Name = Name, Description = Description, IsDefaultPermission = IsDefaultPermission, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, IsDMEnabled = IsDMEnabled, DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified }; @@ -190,13 +193,17 @@ namespace Discord /// If this option is set to autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the name field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. /// The current builder. public SlashCommandBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool? isDefault = null, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -226,6 +233,12 @@ namespace Discord MaxLength = maxLength, }; + if (nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if (descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } @@ -268,6 +281,116 @@ namespace Discord Options.AddRange(options); return this; } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the description field of this command. + /// + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + } + + _descriptionLocalizations = new Dictionary(descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandName(name); + + _nameLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the description field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + internal static void EnsureValidCommandName(string name) + { + Preconditions.NotNullOrEmpty(name, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); + + // Discord updated the docs, this regex prevents special characters like @!$%(... etc, + // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + + if (name.Any(x => char.IsUpper(x))) + throw new FormatException("Name cannot contain any uppercase characters."); + } + + internal static void EnsureValidCommandDescription(string description) + { + Preconditions.NotNullOrEmpty(description, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, MaxDescriptionLength, nameof(description)); + } } /// @@ -287,6 +410,8 @@ namespace Discord private string _name; private string _description; + private Dictionary _nameLocalizations; + private Dictionary _descriptionLocalizations; /// /// Gets or sets the name of this option. @@ -298,10 +423,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxNameLength, nameof(value)); - if (!Regex.IsMatch(value, @"^[\w-]{1,32}$")) - throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(value)); + EnsureValidCommandOptionName(value); } _name = value; @@ -318,8 +440,7 @@ namespace Discord { if (value != null) { - Preconditions.AtLeast(value.Length, 1, nameof(value)); - Preconditions.AtMost(value.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(value)); + EnsureValidCommandOptionDescription(value); } _description = value; @@ -381,6 +502,16 @@ namespace Discord /// public List ChannelTypes { get; set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations => _nameLocalizations; + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations => _descriptionLocalizations; + /// /// Builds the current option. /// @@ -424,6 +555,8 @@ namespace Discord ChannelTypes = ChannelTypes, MinValue = MinValue, MaxValue = MaxValue, + NameLocalizations = _nameLocalizations, + DescriptionLocalizations = _descriptionLocalizations, MinLength = MinLength, MaxLength = MaxLength, }; @@ -440,13 +573,17 @@ namespace Discord /// If this option supports autocomplete. /// The options of the option to add. /// The allowed channel types for this option. + /// Localization dictionary for the description field of this command. + /// Localization dictionary for the description field of this command. /// The choices of this option. /// The smallest number value the user can input. /// The largest number value the user can input. /// The current builder. public SlashCommandOptionBuilder AddOption(string name, ApplicationCommandOptionType type, string description, bool? isRequired = null, bool isDefault = false, bool isAutocomplete = false, double? minValue = null, double? maxValue = null, - int? minLength = null, int? maxLength = null, List options = null, List channelTypes = null, params ApplicationCommandOptionChoiceProperties[] choices) + List options = null, List channelTypes = null, IDictionary nameLocalizations = null, + IDictionary descriptionLocalizations = null, + int? minLength = null, int? maxLength = null, params ApplicationCommandOptionChoiceProperties[] choices) { Preconditions.Options(name, description); @@ -473,9 +610,15 @@ namespace Discord Options = options, Type = type, Choices = (choices ?? Array.Empty()).ToList(), - ChannelTypes = channelTypes + ChannelTypes = channelTypes, }; + if(nameLocalizations is not null) + option.WithNameLocalizations(nameLocalizations); + + if(descriptionLocalizations is not null) + option.WithDescriptionLocalizations(descriptionLocalizations); + return AddOption(option); } /// @@ -522,10 +665,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, int value) + public SlashCommandOptionBuilder AddChoice(string name, int value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -533,10 +677,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary for to use the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, string value) + public SlashCommandOptionBuilder AddChoice(string name, string value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -544,10 +689,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// Localization dictionary for the description field of this command. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, double value) + public SlashCommandOptionBuilder AddChoice(string name, double value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -555,10 +701,11 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, float value) + public SlashCommandOptionBuilder AddChoice(string name, float value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } /// @@ -566,13 +713,14 @@ namespace Discord /// /// The name of the choice. /// The value of the choice. + /// The localization dictionary to use for the name field of this command option choice. /// The current builder. - public SlashCommandOptionBuilder AddChoice(string name, long value) + public SlashCommandOptionBuilder AddChoice(string name, long value, IDictionary nameLocalizations = null) { - return AddChoiceInternal(name, value); + return AddChoiceInternal(name, value, nameLocalizations); } - private SlashCommandOptionBuilder AddChoiceInternal(string name, object value) + private SlashCommandOptionBuilder AddChoiceInternal(string name, object value, IDictionary nameLocalizations = null) { Choices ??= new List(); @@ -594,7 +742,8 @@ namespace Discord Choices.Add(new ApplicationCommandOptionChoiceProperties { Name = name, - Value = value + Value = value, + NameLocalizations = nameLocalizations }); return this; @@ -724,5 +873,107 @@ namespace Discord Type = type; return this; } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the name field of this command option. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithNameLocalizations(IDictionary nameLocalizations) + { + if (nameLocalizations is null) + throw new ArgumentNullException(nameof(nameLocalizations)); + + foreach (var (locale, name) in nameLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + } + + _nameLocalizations = new Dictionary(nameLocalizations); + return this; + } + + /// + /// Sets the collection. + /// + /// The localization dictionary to use for the description field of this command option. + /// The current builder. + /// Thrown if is null. + /// Thrown if any dictionary key is an invalid locale string. + public SlashCommandOptionBuilder WithDescriptionLocalizations(IDictionary descriptionLocalizations) + { + if (descriptionLocalizations is null) + throw new ArgumentNullException(nameof(descriptionLocalizations)); + + foreach (var (locale, description) in _descriptionLocalizations) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + } + + _descriptionLocalizations = new Dictionary(descriptionLocalizations); + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the name field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandOptionBuilder AddNameLocalization(string locale, string name) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionName(name); + + _descriptionLocalizations ??= new(); + _nameLocalizations.Add(locale, name); + + return this; + } + + /// + /// Adds a new entry to the collection. + /// + /// Locale of the entry. + /// Localized string for the description field. + /// The current builder. + /// Thrown if is an invalid locale string. + public SlashCommandOptionBuilder AddDescriptionLocalization(string locale, string description) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + + EnsureValidCommandOptionDescription(description); + + _descriptionLocalizations ??= new(); + _descriptionLocalizations.Add(locale, description); + + return this; + } + + private static void EnsureValidCommandOptionName(string name) + { + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) + throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + } + + private static void EnsureValidCommandOptionDescription(string description) + { + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } } } diff --git a/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs new file mode 100644 index 000000000..75d81d292 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/GenericCollectionExtensions.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace System.Collections.Generic; + +internal static class GenericCollectionExtensions +{ + public static void Deconstruct(this KeyValuePair kvp, out T1 value1, out T2 value2) + { + value1 = kvp.Key; + value2 = kvp.Value; + } + + public static Dictionary ToDictionary(this IEnumerable> kvp) => + kvp.ToDictionary(x => x.Key, x => x.Value); +} diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 14e156769..dd1da3ae3 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -155,12 +155,14 @@ namespace Discord /// /// Gets a collection of all global commands. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global /// application commands. /// - Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null); + Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null); /// /// Creates a global application command. diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index 71010f70d..d28fb707e 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -30,9 +30,13 @@ namespace Discord.Net.Rest /// The cancellation token used to cancel the task. /// Indicates whether to send the header only. /// The audit log reason. + /// Additional headers to be sent with the request. /// - Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null, + IEnumerable>> requestHeaders = null); } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 46aa2681f..ef8dbf756 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,5 +1,6 @@ using Discord.Net; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace Discord /// Gets or sets the maximum time to wait for this request to complete. /// /// - /// Gets or set the max time, in milliseconds, to wait for this request to complete. If + /// Gets or set the max time, in milliseconds, to wait for this request to complete. If /// null, a request will not time out. If a rate limit has been triggered for this request's bucket /// and will not be unpaused in time, this request will fail immediately. /// @@ -53,7 +54,7 @@ namespace Discord /// /// /// This property can also be set in . - /// On a per-request basis, the system clock should only be disabled + /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. /// @@ -70,8 +71,10 @@ namespace Discord internal bool IsReactionBucket { get; set; } internal bool IsGatewayBucket { get; set; } + internal IDictionary> RequestHeaders { get; } + internal static RequestOptions CreateOrClone(RequestOptions options) - { + { if (options == null) return new RequestOptions(); else @@ -96,8 +99,9 @@ namespace Discord public RequestOptions() { Timeout = DiscordConfig.DefaultRequestTimeout; + RequestHeaders = new Dictionary>(); } - + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 2f24e660d..fb855f925 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -55,7 +55,7 @@ namespace Discord if (obj.Value == null) throw CreateNotNullException(name, msg); if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } - } + } private static ArgumentException CreateNotEmptyException(string name, string msg) => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); @@ -129,7 +129,7 @@ namespace Discord private static ArgumentException CreateNotEqualException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value may not be equal to {value}.", paramName: name); - + /// Value must be at least . public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } /// Value must be at least . @@ -165,7 +165,7 @@ namespace Discord private static ArgumentException CreateAtLeastException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at least {value}.", paramName: name); - + /// Value must be greater than . public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } /// Value must be greater than . @@ -201,7 +201,7 @@ namespace Discord private static ArgumentException CreateGreaterThanException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be greater than {value}.", paramName: name); - + /// Value must be at most . public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } /// Value must be at most . @@ -237,7 +237,7 @@ namespace Discord private static ArgumentException CreateAtMostException(string name, string msg, T value) => new ArgumentException(message: msg ?? $"Value must be at most {value}.", paramName: name); - + /// Value must be less than . public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } /// Value must be less than . diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 793d89cdc..50c1f5546 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -83,6 +83,11 @@ namespace Discord.Interactions public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _modalCommandExecutedEvent = new(); + /// + /// Get the used by this Interaction Service instance to localize strings. + /// + public ILocalizationManager LocalizationManager { get; set; } + private readonly ConcurrentDictionary _typedModuleDefs; private readonly CommandMap _slashCommandMap; private readonly ConcurrentDictionary> _contextCommandMaps; @@ -203,6 +208,7 @@ namespace Discord.Interactions _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; + LocalizationManager = config.LocalizationManager; _typeConverterMap = new TypeMap(this, new ConcurrentDictionary { diff --git a/src/Discord.Net.Interactions/InteractionServiceConfig.cs b/src/Discord.Net.Interactions/InteractionServiceConfig.cs index b6576a49f..b9102bc5f 100644 --- a/src/Discord.Net.Interactions/InteractionServiceConfig.cs +++ b/src/Discord.Net.Interactions/InteractionServiceConfig.cs @@ -64,6 +64,11 @@ namespace Discord.Interactions /// Gets or sets whether a command execution should exit when a modal command encounters a missing modal component value. /// public bool ExitOnMissingModalField { get; set; } = false; + + /// + /// Localization provider to be used when registering application commands. + /// + public ILocalizationManager LocalizationManager { get; set; } } /// diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs new file mode 100644 index 000000000..13b155292 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ILocalizationManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// Respresents a localization provider for Discord Application Commands. + /// + public interface ILocalizationManager + { + /// + /// Get every the resource name for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + /// + /// A dictionary containing every available locale and the resource name. + /// + IDictionary GetAllNames(IList key, LocalizationTarget destinationType); + + /// + /// Get every the resource description for every available locale. + /// + /// Location of the resource. + /// Type of the resource. + /// + /// A dictionary containing every available locale and the resource name. + /// + IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType); + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs new file mode 100644 index 000000000..010fb3bdd --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/JsonLocalizationManager.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Json resource files. + /// + public sealed class JsonLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + private const string SpaceToken = "~"; + + private readonly string _basePath; + private readonly string _fileName; + private readonly Regex _localeParserRegex = new Regex(@"\w+.(?\w{2}(?:-\w{2})?).json", RegexOptions.Compiled | RegexOptions.Singleline); + + /// + /// Initializes a new instance of the class. + /// + /// Base path of the Json file. + /// Name of the Json file. + public JsonLocalizationManager(string basePath, string fileName) + { + _basePath = basePath; + _fileName = fileName; + } + + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private string[] GetAllFiles() => + Directory.GetFiles(_basePath, $"{_fileName}.*.json", SearchOption.TopDirectoryOnly); + + private IDictionary GetValues(IList key, string identifier) + { + var result = new Dictionary(); + var files = GetAllFiles(); + + foreach (var file in files) + { + var match = _localeParserRegex.Match(Path.GetFileName(file)); + if (!match.Success) + continue; + + var locale = match.Groups["locale"].Value; + + using var sr = new StreamReader(file); + using var jr = new JsonTextReader(sr); + var obj = JObject.Load(jr); + var token = string.Join(".", key.Select(x => $"['{x}']")) + $".{identifier}"; + var value = (string)obj.SelectToken(token); + if (value is not null) + result[locale] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs new file mode 100644 index 000000000..a110602f2 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationManagers/ResxLocalizationManager.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Discord.Interactions +{ + /// + /// The default localization provider for Resx files. + /// + public sealed class ResxLocalizationManager : ILocalizationManager + { + private const string NameIdentifier = "name"; + private const string DescriptionIdentifier = "description"; + + private readonly ResourceManager _resourceManager; + private readonly IEnumerable _supportedLocales; + + /// + /// Initializes a new instance of the class. + /// + /// Name of the base resource. + /// The main assembly for the resources. + /// Cultures the should search for. + public ResxLocalizationManager(string baseResource, Assembly assembly, params CultureInfo[] supportedLocales) + { + _supportedLocales = supportedLocales; + _resourceManager = new ResourceManager(baseResource, assembly); + } + + /// + public IDictionary GetAllDescriptions(IList key, LocalizationTarget destinationType) => + GetValues(key, DescriptionIdentifier); + + /// + public IDictionary GetAllNames(IList key, LocalizationTarget destinationType) => + GetValues(key, NameIdentifier); + + private IDictionary GetValues(IList key, string identifier) + { + var entryKey = (string.Join(".", key) + "." + identifier); + + var result = new Dictionary(); + + foreach (var locale in _supportedLocales) + { + var value = _resourceManager.GetString(entryKey, locale); + if (value is not null) + result[locale.Name] = value; + } + + return result; + } + } +} diff --git a/src/Discord.Net.Interactions/LocalizationTarget.cs b/src/Discord.Net.Interactions/LocalizationTarget.cs new file mode 100644 index 000000000..cf54d3375 --- /dev/null +++ b/src/Discord.Net.Interactions/LocalizationTarget.cs @@ -0,0 +1,25 @@ +namespace Discord.Interactions +{ + /// + /// Resource targets for localization. + /// + public enum LocalizationTarget + { + /// + /// Target is a tagged with a . + /// + Group, + /// + /// Target is an application command method. + /// + Command, + /// + /// Target is a Slash Command parameter. + /// + Parameter, + /// + /// Target is a Slash Command parameter choice. + /// + Choice + } +} diff --git a/src/Discord.Net.Interactions/Map/CommandMap.cs b/src/Discord.Net.Interactions/Map/CommandMap.cs index 2e7bf5368..336e2b1ec 100644 --- a/src/Discord.Net.Interactions/Map/CommandMap.cs +++ b/src/Discord.Net.Interactions/Map/CommandMap.cs @@ -42,7 +42,7 @@ namespace Discord.Interactions public void RemoveCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.RemoveCommand(key, 0); } @@ -60,28 +60,9 @@ namespace Discord.Interactions private void AddCommand(T command) { - var key = ParseCommandName(command); + var key = CommandHierarchy.GetCommandPath(command); _root.AddCommand(key, 0, command); } - - private IList ParseCommandName(T command) - { - var keywords = new List() { command.Name }; - - var currentParent = command.Module; - - while (currentParent != null) - { - if (!string.IsNullOrEmpty(currentParent.SlashGroupName)) - keywords.Add(currentParent.SlashGroupName); - - currentParent = currentParent.Parent; - } - - keywords.Reverse(); - - return keywords; - } } } diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 409c0e796..9b507f1bb 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Discord.Interactions @@ -9,6 +10,9 @@ namespace Discord.Interactions #region Parameters public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandParameterInfo parameterInfo) { + var localizationManager = parameterInfo.Command.Module.CommandService.LocalizationManager; + var parameterPath = parameterInfo.GetParameterPath(); + var props = new ApplicationCommandOptionProperties { Name = parameterInfo.Name, @@ -18,12 +22,15 @@ namespace Discord.Interactions Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, - Value = x.Value + Value = x.Value, + NameLocalizations = localizationManager?.GetAllNames(parameterInfo.GetChoicePath(x), LocalizationTarget.Choice) ?? ImmutableDictionary.Empty })?.ToList(), ChannelTypes = parameterInfo.ChannelTypes?.ToList(), IsAutocomplete = parameterInfo.IsAutocomplete, MaxValue = parameterInfo.MaxValue, MinValue = parameterInfo.MinValue, + NameLocalizations = localizationManager?.GetAllNames(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(parameterPath, LocalizationTarget.Parameter) ?? ImmutableDictionary.Empty, MinLength = parameterInfo.MinLength, MaxLength = parameterInfo.MaxLength, }; @@ -38,13 +45,19 @@ namespace Discord.Interactions public static SlashCommandProperties ToApplicationCommandProps(this SlashCommandInfo commandInfo) { + var commandPath = commandInfo.GetCommandPath(); + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var props = new SlashCommandBuilder() { Name = commandInfo.Name, Description = commandInfo.Description, + IsDefaultPermission = commandInfo.DefaultPermission, IsDMEnabled = commandInfo.IsEnabledInDm, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), - }.Build(); + }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(); if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -54,18 +67,30 @@ namespace Discord.Interactions return props; } - public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) => - new ApplicationCommandOptionProperties + public static ApplicationCommandOptionProperties ToApplicationCommandOptionProps(this SlashCommandInfo commandInfo) + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return new ApplicationCommandOptionProperties { Name = commandInfo.Name, Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps()) + ?.ToList(), + NameLocalizations = localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty, + DescriptionLocalizations = localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty }; + } public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) - => commandInfo.CommandType switch + { + var localizationManager = commandInfo.Module.CommandService.LocalizationManager; + var commandPath = commandInfo.GetCommandPath(); + + return commandInfo.CommandType switch { ApplicationCommandType.Message => new MessageCommandBuilder { @@ -73,16 +98,21 @@ namespace Discord.Interactions IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(), ApplicationCommandType.User => new UserCommandBuilder { Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), IsDMEnabled = commandInfo.IsEnabledInDm - }.Build(), + } + .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) + .Build(), _ => throw new InvalidOperationException($"{commandInfo.CommandType} isn't a supported command type.") }; + } #endregion #region Modules @@ -123,6 +153,9 @@ namespace Discord.Interactions options.AddRange(moduleInfo.SubModules?.SelectMany(x => x.ParseSubModule(args, ignoreDontRegister))); + var localizationManager = moduleInfo.CommandService.LocalizationManager; + var modulePath = moduleInfo.GetModulePath(); + var props = new SlashCommandBuilder { Name = moduleInfo.SlashGroupName, @@ -130,7 +163,10 @@ namespace Discord.Interactions IsDefaultPermission = moduleInfo.DefaultPermission, IsDMEnabled = moduleInfo.IsEnabledInDm, DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions - }.Build(); + } + .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty) + .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty) + .Build(); if (options.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); @@ -168,7 +204,11 @@ namespace Discord.Interactions Name = moduleInfo.SlashGroupName, Description = moduleInfo.Description, Type = ApplicationCommandOptionType.SubCommandGroup, - Options = options + Options = options, + NameLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllNames(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary.Empty, + DescriptionLocalizations = moduleInfo.CommandService.LocalizationManager?.GetAllDescriptions(moduleInfo.GetModulePath(), LocalizationTarget.Group) + ?? ImmutableDictionary.Empty, } }; } @@ -183,17 +223,29 @@ namespace Discord.Interactions Name = command.Name, Description = command.Description, IsDefaultPermission = command.IsDefaultPermission, - Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, }, ApplicationCommandType.User => new UserCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }, ApplicationCommandType.Message => new MessageCommandProperties { Name = command.Name, - IsDefaultPermission = command.IsDefaultPermission + IsDefaultPermission = command.IsDefaultPermission, + DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsDMEnabled = command.IsEnabledInDm, + NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, + DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty }, _ => throw new InvalidOperationException($"Cannot create command properties for command type {command.Type}"), }; @@ -206,18 +258,20 @@ namespace Discord.Interactions Description = commandOption.Description, Type = commandOption.Type, IsRequired = commandOption.IsRequired, + ChannelTypes = commandOption.ChannelTypes?.ToList(), + IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), + MinValue = commandOption.MinValue, + MaxValue = commandOption.MaxValue, Choices = commandOption.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { Name = x.Name, Value = x.Value }).ToList(), Options = commandOption.Options?.Select(x => x.ToApplicationCommandOptionProps()).ToList(), + NameLocalizations = commandOption.NameLocalizations?.ToImmutableDictionary(), + DescriptionLocalizations = commandOption.DescriptionLocalizations?.ToImmutableDictionary(), MaxLength = commandOption.MaxLength, MinLength = commandOption.MinLength, - MaxValue = commandOption.MaxValue, - MinValue = commandOption.MinValue, - IsAutocomplete = commandOption.IsAutocomplete.GetValueOrDefault(), - ChannelTypes = commandOption.ChannelTypes.ToList(), }; public static Modal ToModal(this ModalInfo modalInfo, string customId, Action modifyModal = null) diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs new file mode 100644 index 000000000..a4554eaef --- /dev/null +++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Interactions +{ + internal static class CommandHierarchy + { + public const char EscapeChar = '$'; + + public static IList GetModulePath(this ModuleInfo moduleInfo) + { + var result = new List(); + + var current = moduleInfo; + while (current is not null) + { + if (current.IsSlashGroup) + result.Insert(0, current.SlashGroupName); + + current = current.Parent; + } + + return result; + } + + public static IList GetCommandPath(this ICommandInfo commandInfo) + { + if (commandInfo.IgnoreGroupNames) + return new string[] { commandInfo.Name }; + + var path = commandInfo.Module.GetModulePath(); + path.Add(commandInfo.Name); + return path; + } + + public static IList GetParameterPath(this IParameterInfo parameterInfo) + { + var path = parameterInfo.Command.GetCommandPath(); + path.Add(parameterInfo.Name); + return path; + } + + public static IList GetChoicePath(this IParameterInfo parameterInfo, ParameterChoice choice) + { + var path = parameterInfo.GetParameterPath(); + path.Add(choice.Name); + return path; + } + + public static IList GetTypePath(Type type) => + new string[] { EscapeChar + type.FullName }; + } +} diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 8b84149dd..e46369277 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -25,6 +26,18 @@ namespace Discord.API [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + // V2 Permissions [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index fff5730f4..fb64d5ebe 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; using System.Linq; namespace Discord.API @@ -38,6 +39,18 @@ namespace Discord.API [JsonProperty("channel_types")] public Optional ChannelTypes { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } + + [JsonProperty("description_localized")] + public Optional DescriptionLocalized { get; set; } + [JsonProperty("min_length")] public Optional MinLength { get; set; } @@ -69,6 +82,11 @@ namespace Discord.API Name = cmd.Name; Type = cmd.Type; Description = cmd.Description; + + NameLocalizations = cmd.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified; + DescriptionLocalizations = cmd.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified; + NameLocalized = cmd.NameLocalized; + DescriptionLocalized = cmd.DescriptionLocalized; } public ApplicationCommandOption(ApplicationCommandOptionProperties option) { @@ -94,6 +112,9 @@ namespace Discord.API Type = option.Type; Description = option.Description; Autocomplete = option.IsAutocomplete; + + NameLocalizations = option.NameLocalizations?.ToDictionary() ?? Optional>.Unspecified; + DescriptionLocalizations = option.DescriptionLocalizations?.ToDictionary() ?? Optional>.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs index 6f84437f6..966405cc9 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -9,5 +10,11 @@ namespace Discord.API [JsonProperty("value")] public object Value { get; set; } + + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("name_localized")] + public Optional NameLocalized { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 7ae8718b6..2257d4b97 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -1,4 +1,8 @@ using Newtonsoft.Json; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Discord.API.Rest { @@ -19,6 +23,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } + [JsonProperty("dm_permission")] public Optional DmPermission { get; set; } @@ -26,12 +36,15 @@ namespace Discord.API.Rest public Optional DefaultMemberPermission { get; set; } public CreateApplicationCommandParams() { } - public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) + public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) { Name = name; Description = description; Options = Optional.Create(options); Type = type; + NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; + DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs index 5891c2c28..f49a3f33d 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rest { @@ -15,5 +16,11 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + + [JsonProperty("name_localizations")] + public Optional> NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public Optional> DescriptionLocalizations { get; set; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index af43e9f4e..686c7b030 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -243,7 +243,7 @@ namespace Discord.Rest => Task.FromResult(null); /// - Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) => Task.FromResult(null); diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index c6ad6a9fb..0c8f8c42f 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -194,10 +194,10 @@ namespace Discord.Rest }; } - public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, - RequestOptions options = null) + public static async Task> GetGlobalApplicationCommandsAsync(BaseDiscordClient client, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); + var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return Array.Empty(); @@ -212,10 +212,10 @@ namespace Discord.Rest return model != null ? RestGlobalCommand.Create(client, model) : null; } - public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, - RequestOptions options = null) + public static async Task> GetGuildApplicationCommandsAsync(BaseDiscordClient client, ulong guildId, bool withLocalizations = false, + string locale = null, RequestOptions options = null) { - var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); + var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, withLocalizations, locale, options).ConfigureAwait(false); if (!response.Any()) return ImmutableArray.Create(); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c5b075103..eb1737c6f 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.Globalization; using System.IO; @@ -1212,11 +1213,22 @@ namespace Discord.API #endregion #region Interactions - public async Task GetGlobalApplicationCommandsAsync(RequestOptions options = null) + public async Task GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands", new BucketIds(), options: options).ConfigureAwait(false); + if (locale is not null) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale)); + + options.RequestHeaders["X-Discord-Locale"] = new[] { locale }; + } + + //with_localizations=false doesnt return localized names and descriptions + var query = withLocalizations ? "?with_localizations=true" : string.Empty; + return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/commands{query}", + new BucketIds(), options: options).ConfigureAwait(false); } public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) @@ -1281,13 +1293,24 @@ namespace Discord.API return await SendJsonAsync("PUT", () => $"applications/{CurrentApplicationId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null) + public async Task GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var bucket = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false); + if (locale is not null) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"{locale} is not a valid locale.", nameof(locale)); + + options.RequestHeaders["X-Discord-Locale"] = new[] { locale }; + } + + //with_localizations=false doesnt return localized names and descriptions + var query = withLocalizations ? "?with_localizations=true" : string.Empty; + return await SendAsync("GET", () => $"applications/{CurrentApplicationId}/guilds/{guildId}/commands{query}", + bucket, options: options).ConfigureAwait(false); } public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index daf7287c7..ddd38c5be 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -25,7 +25,7 @@ namespace Discord.Rest /// Gets the logged-in user. /// public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } - + /// public DiscordRestClient() : this(new DiscordRestConfig()) { } /// @@ -205,10 +205,10 @@ namespace Discord.Rest => ClientHelper.CreateGlobalApplicationCommandAsync(this, properties, options); public Task CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) => ClientHelper.CreateGuildApplicationCommandAsync(this, guildId, properties, options); - public Task> GetGlobalApplicationCommands(RequestOptions options = null) - => ClientHelper.GetGlobalApplicationCommandsAsync(this, options); - public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) - => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, options); + public Task> GetGlobalApplicationCommands(bool withLocalizations = false, string locale = null, RequestOptions options = null) + => ClientHelper.GetGlobalApplicationCommandsAsync(this, withLocalizations, locale, options); + public Task> GetGuildApplicationCommands(ulong guildId, bool withLocalizations = false, string locale = null, RequestOptions options = null) + => ClientHelper.GetGuildApplicationCommandsAsync(this, guildId, withLocalizations, locale, options); public Task> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) => ClientHelper.BulkOverwriteGlobalApplicationCommandAsync(this, commandProperties, options); public Task> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) @@ -319,8 +319,8 @@ namespace Discord.Rest => await GetWebhookAsync(id, options).ConfigureAwait(false); /// - async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) - => await GetGlobalApplicationCommands(options).ConfigureAwait(false); + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) + => await GetGlobalApplicationCommands(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) => await ClientHelper.GetGlobalApplicationCommandAsync(this, id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 8195a2cea..c4e3764d1 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -362,10 +362,10 @@ namespace Discord.Rest #endregion #region Interactions - public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, - RequestOptions options) + public static async Task> GetSlashCommandsAsync(IGuild guild, BaseDiscordClient client, bool withLocalizations, + string locale, RequestOptions options) { - var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, options); + var models = await client.ApiClient.GetGuildApplicationCommandsAsync(guild.Id, withLocalizations, locale, options); return models.Select(x => RestGuildCommand.Create(client, x, guild.Id)).ToImmutableArray(); } public static async Task GetSlashCommandAsync(IGuild guild, ulong id, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 3e0ad1840..eb3254619 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -311,13 +311,15 @@ namespace Discord.Rest /// /// Gets a collection of slash commands created by the current user in this guild. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// - public Task> GetSlashCommandsAsync(RequestOptions options = null) - => GuildHelper.GetSlashCommandsAsync(this, Discord, options); + public Task> GetSlashCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) + => GuildHelper.GetSlashCommandsAsync(this, Discord, withLocalizations, locale, options); /// /// Gets a slash command in the current guild. @@ -928,13 +930,15 @@ namespace Discord.Rest /// /// Gets this guilds slash commands /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - public async Task> GetApplicationCommandsAsync (RequestOptions options = null) - => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, options).ConfigureAwait(false); + public async Task> GetApplicationCommandsAsync (bool withLocalizations = false, string locale = null, RequestOptions options = null) + => await ClientHelper.GetGuildApplicationCommandsAsync(Discord, Id, withLocalizations, locale, options).ConfigureAwait(false); /// /// Gets an application command within this guild with the specified id. /// @@ -1467,8 +1471,8 @@ namespace Discord.Rest async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) - => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) + => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) => await CreateStickerAsync(name, description, tags, image, options); diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 522c098e6..de1ef3149 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -3,6 +3,7 @@ using Discord.API.Rest; using Discord.Net; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -101,11 +102,12 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), DmPermission = arg.IsDMEnabled.ToNullable() - }; if (arg is SlashCommandProperties slashProps) @@ -140,12 +142,16 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), DmPermission = arg.IsDMEnabled.ToNullable() }; + Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); + if (arg is SlashCommandProperties slashProps) { Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); @@ -181,6 +187,8 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), @@ -244,7 +252,9 @@ namespace Discord.Rest Name = args.Name, DefaultPermission = args.IsDefaultPermission.IsSpecified ? args.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + NameLocalizations = args.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() }; if (args is SlashCommandProperties slashProps) @@ -299,6 +309,8 @@ namespace Discord.Rest DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), @@ -335,7 +347,9 @@ namespace Discord.Rest Name = arg.Name, DefaultPermission = arg.IsDefaultPermission.IsSpecified ? arg.IsDefaultPermission.Value - : Optional.Unspecified + : Optional.Unspecified, + NameLocalizations = arg.NameLocalizations?.ToDictionary(), + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() }; if (arg is SlashCommandProperties slashProps) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index 667609ef4..468d10712 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -38,6 +38,32 @@ namespace Discord.Rest /// public IReadOnlyCollection Options { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -64,6 +90,15 @@ namespace Discord.Rest ? model.Options.Value.Select(RestApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs index a40491a2c..b736c435d 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.ApplicationCommandOptionChoice; namespace Discord.Rest @@ -13,10 +15,25 @@ namespace Discord.Rest /// public object Value { get; } + /// + /// Gets the localization dictionary for the name field of this command option choice. + /// + public IReadOnlyDictionary NameLocalizations { get; } + + /// + /// Gets the localized name of this command option choice. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; } + internal RestApplicationCommandChoice(Model model) { Name = model.Name; Value = model.Value; + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); + NameLocalized = model.NameLocalized.GetValueOrDefault(null); } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index c47080be7..3ac15e695 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -27,7 +27,7 @@ namespace Discord.Rest public bool? IsRequired { get; private set; } /// - public bool? IsAutocomplete { get; private set; } + public bool? IsAutocomplete { get; private set; } /// public double? MinValue { get; private set; } @@ -54,6 +54,32 @@ namespace Discord.Rest /// public IReadOnlyCollection ChannelTypes { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + internal RestApplicationCommandOption() { } internal static RestApplicationCommandOption Create(Model model) @@ -98,6 +124,15 @@ namespace Discord.Rest ChannelTypes = model.ChannelTypes.IsSpecified ? model.ChannelTypes.Value.ToImmutableArray() : ImmutableArray.Create(); + + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); } #endregion diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 721c7009d..97872ee6a 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -66,33 +66,45 @@ namespace Discord.Net.Rest _cancelToken = cancelToken; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } - public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } /// Unsupported param type. - public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null, + IEnumerable>> requestHeaders = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); + if (requestHeaders != null) + foreach (var header in requestHeaders) + restRequest.Headers.Add(header.Key, header.Value); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); MemoryStream memoryStream = null; if (multipartParams != null) @@ -126,7 +138,7 @@ namespace Discord.Net.Rest content.Add(streamContent, p.Key, fileValue.Filename); #pragma warning restore IDISP004 - + continue; } default: diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index bb5840ce2..e5cab831e 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -1,5 +1,8 @@ using Discord.Net.Rest; using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; @@ -28,7 +31,7 @@ namespace Discord.Net.Queue public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason, Options.RequestHeaders).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f0b50aa8f..670ed4567 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -450,14 +450,16 @@ namespace Discord.WebSocket /// /// Gets a collection of all global commands. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global /// application commands. /// - public async Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null) + public async Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { - var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); + var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(withLocalizations, locale, options)).Select(x => SocketApplicationCommand.Create(this, x)); foreach(var command in commands) { @@ -3236,8 +3238,8 @@ namespace Discord.WebSocket async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) => await GetGlobalApplicationCommandAsync(id, options); /// - async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) - => await GetGlobalApplicationCommandsAsync(options); + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(bool withLocalizations, string locale, RequestOptions options) + => await GetGlobalApplicationCommandsAsync(withLocalizations, locale, options); /// async Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 78fb33206..55f098b2f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -874,14 +874,17 @@ namespace Discord.WebSocket /// /// Gets a collection of slash commands created by the current user in this guild. /// + /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. + /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// - public async Task> GetApplicationCommandsAsync(RequestOptions options = null) + public async Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { - var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, Id)); + var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, withLocalizations, locale, options)) + .Select(x => SocketApplicationCommand.Create(Discord, x, Id)); foreach (var command in commands) { @@ -1977,8 +1980,8 @@ namespace Discord.WebSocket async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetApplicationCommandsAsync (RequestOptions options) - => await GetApplicationCommandsAsync(options).ConfigureAwait(false); + async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) + => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) => await CreateStickerAsync(name, description, tags, image, options); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index f6b3f9699..b0ddd0012 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -50,6 +50,32 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Options { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -93,6 +119,15 @@ namespace Discord.WebSocket ? model.Options.Value.Select(SocketApplicationCommandOption.Create).ToImmutableArray() : ImmutableArray.Create(); + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); + IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs index e70efa27b..4da1eaadb 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.ApplicationCommandOptionChoice; namespace Discord.WebSocket @@ -13,6 +15,19 @@ namespace Discord.WebSocket /// public object Value { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option choice. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option choice. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + internal SocketApplicationCommandChoice() { } internal static SocketApplicationCommandChoice Create(Model model) { @@ -24,6 +39,8 @@ namespace Discord.WebSocket { Name = model.Name; Value = model.Value; + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary(); + NameLocalized = model.NameLocalized.GetValueOrDefault(null); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs index 478c7cb54..78bb45141 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs @@ -54,6 +54,32 @@ namespace Discord.WebSocket /// public IReadOnlyCollection ChannelTypes { get; private set; } + /// + /// Gets the localization dictionary for the name field of this command option. + /// + public IReadOnlyDictionary NameLocalizations { get; private set; } + + /// + /// Gets the localization dictionary for the description field of this command option. + /// + public IReadOnlyDictionary DescriptionLocalizations { get; private set; } + + /// + /// Gets the localized name of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string NameLocalized { get; private set; } + + /// + /// Gets the localized description of this command option. + /// + /// + /// Only returned when the `withLocalizations` query parameter is set to when requesting the command. + /// + public string DescriptionLocalized { get; private set; } + internal SocketApplicationCommandOption() { } internal static SocketApplicationCommandOption Create(Model model) { @@ -92,6 +118,15 @@ namespace Discord.WebSocket ChannelTypes = model.ChannelTypes.IsSpecified ? model.ChannelTypes.Value.ToImmutableArray() : ImmutableArray.Create(); + + NameLocalizations = model.NameLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + DescriptionLocalizations = model.DescriptionLocalizations.GetValueOrDefault(null)?.ToImmutableDictionary() ?? + ImmutableDictionary.Empty; + + NameLocalized = model.NameLocalized.GetValueOrDefault(); + DescriptionLocalized = model.DescriptionLocalized.GetValueOrDefault(); } IReadOnlyCollection IApplicationCommandOption.Choices => Choices; From adf012d1dd620ed2e4d78a7e6700bbce69a64d38 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sun, 28 Aug 2022 12:04:08 -0300 Subject: [PATCH 140/153] meta: 3.8.0 (#2441) --- CHANGELOG.md | 38 ++++++++++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4022e1b6..7385ab38a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,42 @@ # Changelog + +## [3.8.0] - 2022-08-27 +### Added +- #2384 Added support for the WEBHOOKS_UPDATED event (010e8e8) +- #2370 Add async callbacks for IModuleBase (503fa75) +- #2367 Added DeleteMessagesAsync for TIV and added remaining rate limit in client log (f178660) +- #2379 Added Max/Min length fields for ApplicationCommandOption (e551431) +- #2369 Added support for using `RespondWithModalAsync()` without prior IModal declaration (500e7b4) +- #2347 Added Embed field comparison operators (89a8ea1) +- #2359 Added support for creating lottie stickers (32b03c8) +- #2395 Added App Command localization support and `ILocalizationManager` to IF (39bbd29) + +### Fixed +- #2425 Fix missing Fact attribute in ColorTests (92215b1) +- #2424 Fix IGuild.GetBansAsync() (b7b7964) +- #2416 Fix role icon & emoji assignment (b6b5e95) +- #2414 Fix NRE on RestCommandBase Data (02bc3b7) +- #2421 Fix placeholder length being hardcoded (8dfe19f) +- #2352 Fix issues related to the absence of bot scope (1eb42c6) +- #2346 Fix IGuild.DisconnectAsync(IUser) not disconnecting users (ba02416) +- #2404 Fix range of issues presented by 3rd party analyzer (902326d) +- #2409 Removes GroupContext from requirecontext (b0b8167) + +### Misc +- #2366 Fixed typo in ChannelUpdatedEvent's documentation (cfd2662) +- #2408 Fix sharding sample throwing at appcommand registration (519deda) +- #2420 Fix broken code snippet in dependency injection docs (ddcf68a) +- #2430 Add a note about DontAutoRegisterAttribute (917118d) +- #2418 Update xmldocs to reflect the ConnectedUsers split (65b98f8) +- #2415 Adds missing DI entries in TOC (c49d483) +- #2407 Introduces high quality dependency injection documentation (6fdcf98) +- #2348 Added `RequiredInput` attribute to example in int.framework intro (ee6e0ad) +- #2385 Add ServerStarter.Host to deployment.md (06ed995) +- #2405 Add a note about `IgnoreGroupNames` to IF docs (cf25acd) +- #2356 Makes voice section about precompiled binaries more visible (e0d68d4 ) +- #2405 IF intro docs improvements (246282d) +- #2406 Labs deprecation & readme/docs edits (bf493ea) + ## [3.7.2] - 2022-06-02 ### Added - #2328 Add method overloads to InteractionService (0fad3e8) diff --git a/Discord.Net.targets b/Discord.Net.targets index 8cedb40e7..d9ec415f9 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.7.2 + 3.8.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 5dd1e640d..64a20ecb2 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.7.2", + "_appFooter": "Discord.Net (c) 2015-2022 3.8.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 1a61ff97a..63a288dc3 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.7.2$suffix$ + 3.8.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 0aa381d4683694ab85e300337aef4be42a171e33 Mon Sep 17 00:00:00 2001 From: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:24:32 +0200 Subject: [PATCH 141/153] Fix typos of word `length` (#2443) --- .../SlashCommands/SlashCommandBuilder.cs | 6 +++--- .../Attributes/MaxLengthAttribute.cs | 6 +++--- .../Attributes/MinLengthAttribute.cs | 6 +++--- .../Modals/Inputs/TextInputComponentBuilder.cs | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index 579289304..b443c4468 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -855,11 +855,11 @@ namespace Discord /// /// Sets the current builders max length field. /// - /// The value to set. + /// The value to set. /// The current builder. - public SlashCommandOptionBuilder WithMaxLength(int lenght) + public SlashCommandOptionBuilder WithMaxLength(int length) { - MaxLength = lenght; + MaxLength = length; return this; } diff --git a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs index 1099e7d92..2172886d2 100644 --- a/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/MaxLengthAttribute.cs @@ -16,10 +16,10 @@ namespace Discord.Interactions /// /// Sets the maximum length allowed for a string type parameter. /// - /// Maximum string length allowed. - public MaxLengthAttribute(int lenght) + /// Maximum string length allowed. + public MaxLengthAttribute(int length) { - Length = lenght; + Length = length; } } } diff --git a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs index 7d0b0fd63..8050f992a 100644 --- a/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/MinLengthAttribute.cs @@ -16,10 +16,10 @@ namespace Discord.Interactions /// /// Sets the minimum length allowed for a string type parameter. /// - /// Minimum string length allowed. - public MinLengthAttribute(int lenght) + /// Minimum string length allowed. + public MinLengthAttribute(int length) { - Length = lenght; + Length = length; } } } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs index 8dd2c4004..728b97a7a 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs @@ -67,26 +67,26 @@ namespace Discord.Interactions.Builders /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// - public TextInputComponentBuilder WithMinLenght(int minLenght) + public TextInputComponentBuilder WithMinLength(int minLength) { - MinLength = minLenght; + MinLength = minLength; return this; } /// /// Sets . /// - /// New value of the . + /// New value of the . /// /// The builder instance. /// - public TextInputComponentBuilder WithMaxLenght(int maxLenght) + public TextInputComponentBuilder WithMaxLength(int maxLength) { - MaxLength = maxLenght; + MaxLength = maxLength; return this; } From 9feb703a82d6bd3c15118237c260320c8e873269 Mon Sep 17 00:00:00 2001 From: Viktor Chernikov <96922685+Polybroo@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:25:13 +0200 Subject: [PATCH 142/153] Wrong symbol fix (#2438) --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index eb1737c6f..615e5ac12 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -862,7 +862,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); - await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}${WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// This operation may only be called with a token. From fbc5ad414f87b2d4678b697f05ed06d70d8b9b16 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:24:33 +0300 Subject: [PATCH 143/153] fix BulkOverwriteCommands NRE (#2444) --- src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index de1ef3149..deca00b72 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -150,8 +150,6 @@ namespace Discord.Rest DmPermission = arg.IsDMEnabled.ToNullable() }; - Console.WriteLine("Locales:" + string.Join(",", arg.NameLocalizations.Keys)); - if (arg is SlashCommandProperties slashProps) { Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); From 370bdfa3c630152bfcd851c918f5d49ab6a3116d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 1 Sep 2022 20:48:00 +0200 Subject: [PATCH 144/153] Bump to Discord API v10 (#2448) * Bump, add messagecontent intent * Update comments Co-authored-by: Rozen --- src/Discord.Net.Core/DiscordConfig.cs | 2 +- src/Discord.Net.Core/Entities/Messages/IMessage.cs | 6 ++++++ src/Discord.Net.Core/GatewayIntents.cs | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 2db802f1e..ebca0120c 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -18,7 +18,7 @@ namespace Discord /// Discord API documentation /// . /// - public const int APIVersion = 9; + public const int APIVersion = 10; /// /// Returns the Voice API version Discord.Net uses. /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index f5f2ca007..48db4fdf0 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -48,6 +48,9 @@ namespace Discord /// /// Gets the content for this message. /// + /// + /// This will be empty if the privileged is disabled. + /// /// /// A string that contains the body of the message; note that this field may be empty if there is an embed. /// @@ -55,6 +58,9 @@ namespace Discord /// /// Gets the clean content for this message. /// + /// + /// This will be empty if the privileged is disabled. + /// /// /// A string that contains the body of the message stripped of mentions, markdown, emojis and pings; note that this field may be empty if there is an embed. /// diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs index f2a99e44c..e9dd8f814 100644 --- a/src/Discord.Net.Core/GatewayIntents.cs +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -39,7 +39,14 @@ namespace Discord DirectMessageReactions = 1 << 13, /// This intent includes TYPING_START DirectMessageTyping = 1 << 14, - /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + /// + /// This intent defines if the content within messages received by MESSAGE_CREATE is available or not. + /// This is a privileged intent and needs to be enabled in the developer portal. + /// + MessageContent = 1 << 15, + /// + /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE + /// GuildScheduledEvents = 1 << 16, /// /// This intent includes all but and @@ -51,6 +58,6 @@ namespace Discord /// /// This intent includes all of them, including privileged ones. /// - All = AllUnprivileged | GuildMembers | GuildPresences + All = AllUnprivileged | GuildMembers | GuildPresences | MessageContent } } From 376a812b6a3b435566a2abbb011968dafb1db3d5 Mon Sep 17 00:00:00 2001 From: Damian Kraaijeveld Date: Fri, 2 Sep 2022 23:04:53 +0200 Subject: [PATCH 145/153] Return a list instead of an array (#2451) --- src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs index a4554eaef..da7ef22e0 100644 --- a/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs +++ b/src/Discord.Net.Interactions/Utilities/CommandHierarchy.cs @@ -26,7 +26,7 @@ namespace Discord.Interactions public static IList GetCommandPath(this ICommandInfo commandInfo) { if (commandInfo.IgnoreGroupNames) - return new string[] { commandInfo.Name }; + return new List { commandInfo.Name }; var path = commandInfo.Module.GetModulePath(); path.Add(commandInfo.Name); @@ -48,6 +48,6 @@ namespace Discord.Interactions } public static IList GetTypePath(Type type) => - new string[] { EscapeChar + type.FullName }; + new List { EscapeChar + type.FullName }; } } From b967e6907c3ff204705bd60e50f7e31d6071cc95 Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:14:16 -0300 Subject: [PATCH 146/153] [Robot] Add missing json error (#2447) * Add 20024, 30032, 30034, 30052, 40012, 40043, 40058, 40066, 40067, 50017, 50132, 50138, 50146, 110001, 200000, 200001, 220001, 220002, 220003, 220004, 240000 Error codes * Update src/Discord.Net.Core/DiscordErrorCode.cs * Apply suggestions from code review Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index b444614e4..262252eab 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -66,6 +66,7 @@ namespace Discord ActionSlowmode = 20016, OnlyOwnerAction = 20018, AnnouncementEditRatelimit = 20022, + UnderMinimumAge = 20024, ChannelWriteRatelimit = 20028, WriteRatelimitReached = 20029, WordsNotAllowed = 20031, @@ -88,7 +89,9 @@ namespace Discord MaximumServerMembersReached = 30019, MaximumServerCategoriesReached = 30030, GuildTemplateAlreadyExists = 30031, + MaximumNumberOfApplicationCommandsReached = 30032, MaximumThreadMembersReached = 30033, + MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034, MaximumBansForNonGuildMembersReached = 30035, MaximumBanFetchesReached = 30037, MaximumUncompleteGuildScheduledEvents = 30038, @@ -98,6 +101,7 @@ namespace Discord #endregion #region General Request Errors (40XXX) + BitrateIsTooHighForChannelOfThisType = 30052, MaximumNumberOfEditsReached = 30046, MaximumNumberOfPinnedThreadsInAForumChannelReached = 30047, MaximumNumberOfTagsInAForumChannelReached = 30048, @@ -108,12 +112,17 @@ namespace Discord RequestEntityTooLarge = 40005, FeatureDisabled = 40006, UserBanned = 40007, + ConnectionHasBeenRevoked = 40012, TargetUserNotInVoice = 40032, MessageAlreadyCrossposted = 40033, ApplicationNameAlreadyExists = 40041, #endregion #region Action Preconditions/Checks (50XXX) + ApplicationInteractionFailedToSend = 40043, + CannotSendAMessageInAForumChannel = 40058, + ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066, + ATagIsRequiredToCreateAForumPostInThisChannel = 40067, InteractionHasAlreadyBeenAcknowledged = 40060, TagNamesMustBeUnique = 40061, MissingPermissions = 50001, @@ -132,6 +141,7 @@ namespace Discord InvalidAuthenticationToken = 50014, NoteTooLong = 50015, ProvidedMessageDeleteCountOutOfBounds = 50016, + InvalidMFALevel = 50017, InvalidPinChannel = 50019, InvalidInvite = 50020, CannotExecuteOnSystemMessage = 50021, @@ -165,6 +175,9 @@ namespace Discord #endregion #region 2FA (60XXX) + OwnershipCannotBeTransferredToABotUser = 50132, + AssetResizeBelowTheMaximumSize= 50138, + UploadedFileNotFound = 50146, MissingPermissionToSendThisSticker = 50600, Requires2FA = 60003, #endregion @@ -178,6 +191,7 @@ namespace Discord #endregion #region API Status (130XXX) + ApplicationNotYetAvailable = 110001, APIOverloaded = 130000, #endregion @@ -207,5 +221,15 @@ namespace Discord CannotUpdateFinishedEvent = 180000, FailedStageCreation = 180002, #endregion + + #region Forum & Automod + MessageWasBlockedByAutomaticModeration = 200000, + TitleWasBlockedByAutomaticModeration = 200001, + WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId = 220001, + WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId = 220002, + WebhooksCanOnlyCreateThreadsInForumChannels = 220003, + WebhookServicesCannotBeUsedInForumChannels = 220004, + MessageBlockedByHarmfulLinksFilter = 240000, + #endregion } } From fca9c6b618715baaeeb5602d70fb8acfd91f6fe3 Mon Sep 17 00:00:00 2001 From: Julian <54598714+akaJuliaan@users.noreply.github.com> Date: Fri, 2 Sep 2022 23:31:51 +0200 Subject: [PATCH 147/153] Fix: remove Module from _typedModuleDefs (#2417) --- src/Discord.Net.Commands/CommandService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 57e0e430e..29bf6a428 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -270,6 +270,11 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { + var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); + + if (!typeModulePair.Equals(default(KeyValuePair))) + _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); + return RemoveModuleInternal(module); } finally From 3dec99f6dfb5902b823b6f1b101a7d7361b65efd Mon Sep 17 00:00:00 2001 From: SaculRennorb Date: Fri, 2 Sep 2022 23:56:19 +0200 Subject: [PATCH 148/153] adding scheduled events to audit log (#2437) * first draft * made changes be actually optional. not everything always changes * 'doc' text * more 'doc' stuff * more 'doc' stuff3 * 'doc' stuff --- .../Entities/AuditLogs/ActionType.cs | 14 ++ .../Entities/AuditLogs/AuditLogHelper.cs | 4 + .../ScheduledEventCreateAuditLogData.cs | 149 ++++++++++++++++++ .../ScheduledEventDeleteAuditLogData.cs | 34 ++++ .../AuditLogs/DataTypes/ScheduledEventInfo.cs | 80 ++++++++++ .../ScheduledEventUpdateAuditLogData.cs | 99 ++++++++++++ 6 files changed, 380 insertions(+) create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventCreateAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventDeleteAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventInfo.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventUpdateAuditLogData.cs diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index 5092b4e7f..ad2d659ee 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -180,6 +180,20 @@ namespace Discord /// A sticker was deleted. /// StickerDeleted = 92, + + /// + /// A scheduled event was created. + /// + EventCreate = 100, + /// + /// A scheduled event was created. + /// + EventUpdate = 101, + /// + /// A scheduled event was created. + /// + EventDelete = 102, + /// /// A thread was created. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index edbb2bea8..f071fc1f9 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -52,6 +52,10 @@ namespace Discord.Rest [ActionType.MessagePinned] = MessagePinAuditLogData.Create, [ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create, + [ActionType.EventCreate] = ScheduledEventCreateAuditLogData.Create, + [ActionType.EventUpdate] = ScheduledEventUpdateAuditLogData.Create, + [ActionType.EventDelete] = ScheduledEventDeleteAuditLogData.Create, + [ActionType.ThreadCreate] = ThreadCreateAuditLogData.Create, [ActionType.ThreadUpdate] = ThreadUpdateAuditLogData.Create, [ActionType.ThreadDelete] = ThreadDeleteAuditLogData.Create, diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventCreateAuditLogData.cs new file mode 100644 index 000000000..11faa3371 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventCreateAuditLogData.cs @@ -0,0 +1,149 @@ +using System; +using System.Linq; +using Discord.API; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a scheduled event creation. + /// + public class ScheduledEventCreateAuditLogData : IAuditLogData + { + private ScheduledEventCreateAuditLogData(ulong id, ulong guildId, ulong? channelId, ulong? creatorId, string name, string description, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime, GuildScheduledEventPrivacyLevel privacyLevel, GuildScheduledEventStatus status, GuildScheduledEventType entityType, ulong? entityId, string location, RestUser creator, int userCount, string image) + { + Id = id ; + GuildId = guildId ; + ChannelId = channelId ; + CreatorId = creatorId ; + Name = name ; + Description = description ; + ScheduledStartTime = scheduledStartTime; + ScheduledEndTime = scheduledEndTime ; + PrivacyLevel = privacyLevel ; + Status = status ; + EntityType = entityType ; + EntityId = entityId ; + Location = location ; + Creator = creator ; + UserCount = userCount ; + Image = image ; + } + + internal static ScheduledEventCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var id = entry.TargetId.Value; + + var guildId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "guild_id") + .NewValue.ToObject(discord.ApiClient.Serializer); + var channelId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "channel_id") + .NewValue.ToObject(discord.ApiClient.Serializer); + var creatorId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "channel_id") + .NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(); + var name = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name") + .NewValue.ToObject(discord.ApiClient.Serializer); + var description = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "description") + .NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(); + var scheduledStartTime = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "scheduled_start_time") + .NewValue.ToObject(discord.ApiClient.Serializer); + var scheduledEndTime = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "scheduled_end_time") + .NewValue.ToObject(discord.ApiClient.Serializer); + var privacyLevel = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "privacy_level") + .NewValue.ToObject(discord.ApiClient.Serializer); + var status = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "status") + .NewValue.ToObject(discord.ApiClient.Serializer); + var entityType = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_type") + .NewValue.ToObject(discord.ApiClient.Serializer); + var entityId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_id") + .NewValue.ToObject(discord.ApiClient.Serializer); + var entityMetadata = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_metadata") + .NewValue.ToObject(discord.ApiClient.Serializer); + var creator = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "creator") + .NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(); + var userCount = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "user_count") + .NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(); + var image = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "image") + .NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(); + + var creatorUser = creator == null ? null : RestUser.Create(discord, creator); + + return new ScheduledEventCreateAuditLogData(id, guildId, channelId, creatorId, name, description, scheduledStartTime, scheduledEndTime, privacyLevel, status, entityType, entityId, entityMetadata.Location.GetValueOrDefault(), creatorUser, userCount, image); + } + + // Doc Note: Corresponds to the *current* data + + /// + /// Gets the snowflake id of the event. + /// + public ulong Id { get; } + /// + /// Gets the snowflake id of the guild the event is associated with. + /// + public ulong GuildId { get; } + /// + /// Gets the snowflake id of the channel the event is associated with. + /// + public ulong? ChannelId { get; } + /// + /// Gets the snowflake id of the original creator of the event. + /// + public ulong? CreatorId { get; } + /// + /// Gets name of the event. + /// + public string Name { get; } + /// + /// Gets the description of the event. null if none is set. + /// + public string Description { get; } + /// + /// Gets the time the event was scheduled for. + /// + public DateTimeOffset ScheduledStartTime { get; } + /// + /// Gets the time the event was scheduled to end. + /// + public DateTimeOffset? ScheduledEndTime { get; } + /// + /// Gets the privacy level of the event. + /// + public GuildScheduledEventPrivacyLevel PrivacyLevel { get; } + /// + /// Gets the status of the event. + /// + public GuildScheduledEventStatus Status { get; } + /// + /// Gets the type of the entity associated with the event (stage / void / external). + /// + public GuildScheduledEventType EntityType { get; } + /// + /// Gets the snowflake id of the entity associated with the event (stage / void / external). + /// + public ulong? EntityId { get; } + /// + /// Gets the metadata for the entity associated with the event. + /// + public string Location { get; } + /// + /// Gets the user that originally created the event. + /// + public RestUser Creator { get; } + /// + /// Gets the count of users interested in this event. + /// + public int UserCount { get; } + /// + /// Gets the image hash of the image that was attached to the event. Null if not set. + /// + public string Image { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventDeleteAuditLogData.cs new file mode 100644 index 000000000..34fa96225 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventDeleteAuditLogData.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using Discord.API; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a scheduled event deleteion. + /// + public class ScheduledEventDeleteAuditLogData : IAuditLogData + { + private ScheduledEventDeleteAuditLogData(ulong id) + { + Id = id; + } + + internal static ScheduledEventDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var id = entry.TargetId.Value; + + return new ScheduledEventDeleteAuditLogData(id); + } + + // Doc Note: Corresponds to the *current* data + + /// + /// Gets the snowflake id of the event. + /// + public ulong Id { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventInfo.cs new file mode 100644 index 000000000..a45956546 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventInfo.cs @@ -0,0 +1,80 @@ +using System; + +namespace Discord.Rest +{ + /// + /// Represents information for a scheduled event. + /// + public class ScheduledEventInfo + { + /// + /// Gets the snowflake id of the guild the event is associated with. + /// + public ulong? GuildId { get; } + /// + /// Gets the snowflake id of the channel the event is associated with. + /// + public ulong? ChannelId { get; } + /// + /// Gets name of the event. + /// + public string Name { get; } + /// + /// Gets the description of the event. null if none is set. + /// + public string Description { get; } + /// + /// Gets the time the event was scheduled for. + /// + public DateTimeOffset? ScheduledStartTime { get; } + /// + /// Gets the time the event was scheduled to end. + /// + public DateTimeOffset? ScheduledEndTime { get; } + /// + /// Gets the privacy level of the event. + /// + public GuildScheduledEventPrivacyLevel? PrivacyLevel { get; } + /// + /// Gets the status of the event. + /// + public GuildScheduledEventStatus? Status { get; } + /// + /// Gets the type of the entity associated with the event (stage / void / external). + /// + public GuildScheduledEventType? EntityType { get; } + /// + /// Gets the snowflake id of the entity associated with the event (stage / void / external). + /// + public ulong? EntityId { get; } + /// + /// Gets the metadata for the entity associated with the event. + /// + public string Location { get; } + /// + /// Gets the count of users interested in this event. + /// + public int? UserCount { get; } + /// + /// Gets the image hash of the image that was attached to the event. Null if not set. + /// + public string Image { get; } + + internal ScheduledEventInfo(ulong? guildId, ulong? channelId, string name, string description, DateTimeOffset? scheduledStartTime, DateTimeOffset? scheduledEndTime, GuildScheduledEventPrivacyLevel? privacyLevel, GuildScheduledEventStatus? status, GuildScheduledEventType? entityType, ulong? entityId, string location, int? userCount, string image) + { + GuildId = guildId ; + ChannelId = channelId ; + Name = name ; + Description = description ; + ScheduledStartTime = scheduledStartTime; + ScheduledEndTime = scheduledEndTime ; + PrivacyLevel = privacyLevel ; + Status = status ; + EntityType = entityType ; + EntityId = entityId ; + Location = location ; + UserCount = userCount ; + Image = image ; + } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventUpdateAuditLogData.cs new file mode 100644 index 000000000..2ef2ccff8 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ScheduledEventUpdateAuditLogData.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using Discord.API; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a scheduled event updates. + /// + public class ScheduledEventUpdateAuditLogData : IAuditLogData + { + private ScheduledEventUpdateAuditLogData(ulong id, ScheduledEventInfo before, ScheduledEventInfo after) + { + Id = id; + Before = before; + After = after; + } + + internal static ScheduledEventUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var changes = entry.Changes; + + var id = entry.TargetId.Value; + + var guildId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "guild_id"); + var channelId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "channel_id"); + var name = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); + var description = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "description"); + var scheduledStartTime = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "scheduled_start_time"); + var scheduledEndTime = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "scheduled_end_time"); + var privacyLevel = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "privacy_level"); + var status = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "status"); + var entityType = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_type"); + var entityId = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_id"); + var entityMetadata = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "entity_metadata"); + var userCount = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "user_count"); + var image = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "image"); + + var before = new ScheduledEventInfo( + guildId?.OldValue.ToObject(discord.ApiClient.Serializer), + channelId?.OldValue.ToObject(discord.ApiClient.Serializer), + name?.OldValue.ToObject(discord.ApiClient.Serializer), + description?.OldValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(), + scheduledStartTime?.OldValue.ToObject(discord.ApiClient.Serializer), + scheduledEndTime?.OldValue.ToObject(discord.ApiClient.Serializer), + privacyLevel?.OldValue.ToObject(discord.ApiClient.Serializer), + status?.OldValue.ToObject(discord.ApiClient.Serializer), + entityType?.OldValue.ToObject(discord.ApiClient.Serializer), + entityId?.OldValue.ToObject(discord.ApiClient.Serializer), + entityMetadata?.OldValue.ToObject(discord.ApiClient.Serializer) + ?.Location.GetValueOrDefault(), + userCount?.OldValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(), + image?.OldValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault() + ); + var after = new ScheduledEventInfo( + guildId?.NewValue.ToObject(discord.ApiClient.Serializer), + channelId?.NewValue.ToObject(discord.ApiClient.Serializer), + name?.NewValue.ToObject(discord.ApiClient.Serializer), + description?.NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(), + scheduledStartTime?.NewValue.ToObject(discord.ApiClient.Serializer), + scheduledEndTime?.NewValue.ToObject(discord.ApiClient.Serializer), + privacyLevel?.NewValue.ToObject(discord.ApiClient.Serializer), + status?.NewValue.ToObject(discord.ApiClient.Serializer), + entityType?.NewValue.ToObject(discord.ApiClient.Serializer), + entityId?.NewValue.ToObject(discord.ApiClient.Serializer), + entityMetadata?.NewValue.ToObject(discord.ApiClient.Serializer) + ?.Location.GetValueOrDefault(), + userCount?.NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault(), + image?.NewValue.ToObject>(discord.ApiClient.Serializer) + .GetValueOrDefault() + ); + + return new ScheduledEventUpdateAuditLogData(id, before, after); + } + + // Doc Note: Corresponds to the *current* data + + /// + /// Gets the snowflake id of the event. + /// + public ulong Id { get; } + /// + /// Gets the state before the change. + /// + public ScheduledEventInfo Before { get; } + /// + /// Gets the state after the change. + /// + public ScheduledEventInfo After { get; } + } +} From 11ece4bf169dd91cd13f49d2ce2f27b0c14bb6d6 Mon Sep 17 00:00:00 2001 From: d4n Date: Fri, 2 Sep 2022 21:47:13 -0500 Subject: [PATCH 149/153] Update app commands regex and fix localization on app context commands (#2452) Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- .../Interactions/ApplicationCommandOption.cs | 6 ++-- .../ApplicationCommandProperties.cs | 5 ++-- .../ContextMenus/MessageCommandBuilder.cs | 11 ++----- .../ContextMenus/UserCommandBuilder.cs | 11 ++----- .../SlashCommands/SlashCommandBuilder.cs | 29 +++++++++---------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index bceefda32..df33cfe1d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -145,10 +145,10 @@ namespace Discord if (name.Length > 32) throw new ArgumentOutOfRangeException(nameof(name), "Name length must be less than or equal to 32."); - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new FormatException($"{nameof(name)} must match the regex ^[\\w-]{{1,32}}$"); + if (!Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); - if (name.Any(x => char.IsUpper(x))) + if (name.Any(char.IsUpper)) throw new FormatException("Name cannot contain any uppercase characters."); } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 7ca16a27d..98e050df9 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -42,8 +42,9 @@ namespace Discord Preconditions.AtLeast(name.Length, 1, nameof(name)); Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + + if (Type == ApplicationCommandType.Slash && !Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); } _nameLocalizations = value; } diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index ed49c685d..613e30376 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -67,7 +67,8 @@ namespace Discord Name = Name, IsDefaultPermission = IsDefaultPermission, IsDMEnabled = IsDMEnabled, - DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified, + NameLocalizations = NameLocalizations }; return props; @@ -157,14 +158,6 @@ namespace Discord Preconditions.NotNullOrEmpty(name, nameof(name)); Preconditions.AtLeast(name.Length, 1, nameof(name)); Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); - - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - - if (name.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); } /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index d8bb2e056..8ac524582 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -65,7 +65,8 @@ namespace Discord Name = Name, IsDefaultPermission = IsDefaultPermission, IsDMEnabled = IsDMEnabled, - DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified, + NameLocalizations = NameLocalizations }; return props; @@ -155,14 +156,6 @@ namespace Discord Preconditions.NotNullOrEmpty(name, nameof(name)); Preconditions.AtLeast(name.Length, 1, nameof(name)); Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); - - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); - - if (name.Any(x => char.IsUpper(x))) - throw new FormatException("Name cannot contain any uppercase characters."); } /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index b443c4468..1df886abe 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -207,10 +207,9 @@ namespace Discord { Preconditions.Options(name, description); - // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + // https://discord.com/developers/docs/interactions/application-commands + if (!Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); // make sure theres only one option with default set to true if (isDefault == true && Options?.Any(x => x.IsDefault == true) == true) @@ -376,12 +375,11 @@ namespace Discord Preconditions.AtLeast(name.Length, 1, nameof(name)); Preconditions.AtMost(name.Length, MaxNameLength, nameof(name)); - // Discord updated the docs, this regex prevents special characters like @!$%(... etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + // https://discord.com/developers/docs/interactions/application-commands + if (!Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); - if (name.Any(x => char.IsUpper(x))) + if (name.Any(char.IsUpper)) throw new FormatException("Name cannot contain any uppercase characters."); } @@ -587,10 +585,9 @@ namespace Discord { Preconditions.Options(name, description); - // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc, - // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Command name cannot contain any special characters or whitespaces!", nameof(name)); + // https://discord.com/developers/docs/interactions/application-commands + if (!Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); // make sure theres only one option with default set to true if (isDefault && Options?.Any(x => x.IsDefault == true) == true) @@ -966,8 +963,10 @@ namespace Discord { Preconditions.AtLeast(name.Length, 1, nameof(name)); Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); - if (!Regex.IsMatch(name, @"^[\w-]{1,32}$")) - throw new ArgumentException("Option name cannot contain any special characters or whitespaces!", nameof(name)); + + // https://discord.com/developers/docs/interactions/application-commands + if (!Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); } private static void EnsureValidCommandOptionDescription(string description) From 2b86a79f7008c4f4fef49cec144c95d5e3bde23f Mon Sep 17 00:00:00 2001 From: Proddy Date: Sun, 4 Sep 2022 07:46:50 +0100 Subject: [PATCH 150/153] Fix a bug in EmbedBuilder.Length when there is an EmbedField with no Value (#2345) * Update EmbedBuilder.cs Fixes a bug where 'EmbedBuilder.Length' will throw an exception of type 'System.NullReferenceException' when a field doesn't have a value. * Update EmbedBuilder.cs Fixed an incorrect assuption that `Value` was a `string?` * Update EmbedBuilder.cs Fixed one more null check * Update EmbedBuilder.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index db38b9fb7..9b2a6adb9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -150,7 +150,7 @@ namespace Discord int authorLength = Author?.Name?.Length ?? 0; int descriptionLength = Description?.Length ?? 0; int footerLength = Footer?.Text?.Length ?? 0; - int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length); + int fieldSum = Fields.Sum(f => f.Name.Length + (f.Value?.ToString()?.Length ?? 0)); return titleLength + authorLength + descriptionLength + footerLength + fieldSum; } From 5073afa3168b4aa31a7427e38bf698b5f45b0525 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Sat, 3 Sep 2022 23:50:29 -0700 Subject: [PATCH 151/153] Update PackageLogo.png (#2333) --- docs/marketing/logo/PackageLogo.png | Bin 11639 -> 77377 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/marketing/logo/PackageLogo.png b/docs/marketing/logo/PackageLogo.png index 047d6ad64f279e7475a6b031d915f52c1143bc6b..6311e6eca797624c3af69862c80f8ab27faaee8a 100644 GIT binary patch literal 77377 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 From 4834b27f9021bfa75524ac889acb1e2948a604f6 Mon Sep 17 00:00:00 2001 From: essoperagma Date: Fri, 9 Sep 2022 10:20:19 +0000 Subject: [PATCH 152/153] Null check slash command localizations (#2453) --- .../Interactions/ApplicationCommandOption.cs | 24 ++++++++---- .../ApplicationCommandOptionChoice.cs | 23 +++++++----- .../ApplicationCommandProperties.cs | 32 ++++++++++------ .../SlashCommands/SlashCommandBuilder.cs | 2 +- .../CommandBuilderTests.cs | 37 +++++++++++++++++++ 5 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 test/Discord.Net.Tests.Unit/CommandBuilderTests.cs diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs index df33cfe1d..17e836e21 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs @@ -106,13 +106,17 @@ namespace Discord get => _nameLocalizations; set { - foreach (var (locale, name) in value) + if (value != null) { - if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) - throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + foreach (var (locale, name) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); - EnsureValidOptionName(name); + EnsureValidOptionName(name); + } } + _nameLocalizations = value; } } @@ -126,13 +130,17 @@ namespace Discord get => _descriptionLocalizations; set { - foreach (var (locale, description) in value) + if (value != null) { - if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) - throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + foreach (var (locale, description) in value) + { + if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); - EnsureValidOptionDescription(description); + EnsureValidOptionDescription(description); + } } + _descriptionLocalizations = value; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs index 8f1ecc6d2..2289b412d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs @@ -55,18 +55,21 @@ namespace Discord get => _nameLocalizations; set { - foreach (var (locale, name) in value) + if (value != null) { - if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) - throw new ArgumentException("Key values of the dictionary must be valid language codes."); - - switch (name.Length) + foreach (var (locale, name) in value) { - case > 100: - throw new ArgumentOutOfRangeException(nameof(value), - "Name length must be less than or equal to 100."); - case 0: - throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException("Key values of the dictionary must be valid language codes."); + + switch (name.Length) + { + case > 100: + throw new ArgumentOutOfRangeException(nameof(value), + "Name length must be less than or equal to 100."); + case 0: + throw new ArgumentOutOfRangeException(nameof(value), "Name length must at least 1."); + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 98e050df9..0c1c628cd 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -35,17 +35,21 @@ namespace Discord get => _nameLocalizations; set { - foreach (var (locale, name) in value) + if (value != null) { - if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) - throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + foreach (var (locale, name) in value) + { + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); - Preconditions.AtLeast(name.Length, 1, nameof(name)); - Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); + Preconditions.AtLeast(name.Length, 1, nameof(name)); + Preconditions.AtMost(name.Length, SlashCommandBuilder.MaxNameLength, nameof(name)); - if (Type == ApplicationCommandType.Slash && !Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) - throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); + if (Type == ApplicationCommandType.Slash && !Regex.IsMatch(name, @"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")) + throw new ArgumentException(@"Name must match the regex ^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$", nameof(name)); + } } + _nameLocalizations = value; } } @@ -58,14 +62,18 @@ namespace Discord get => _descriptionLocalizations; set { - foreach (var (locale, description) in value) + if (value != null) { - if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) - throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); + foreach (var (locale, description) in value) + { + if (!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) + throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); - Preconditions.AtLeast(description.Length, 1, nameof(description)); - Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + Preconditions.AtLeast(description.Length, 1, nameof(description)); + Preconditions.AtMost(description.Length, SlashCommandBuilder.MaxDescriptionLength, nameof(description)); + } } + _descriptionLocalizations = value; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index 1df886abe..94f5956dd 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -907,7 +907,7 @@ namespace Discord if (descriptionLocalizations is null) throw new ArgumentNullException(nameof(descriptionLocalizations)); - foreach (var (locale, description) in _descriptionLocalizations) + foreach (var (locale, description) in descriptionLocalizations) { if(!Regex.IsMatch(locale, @"^\w{2}(?:-\w{2})?$")) throw new ArgumentException($"Invalid locale: {locale}", nameof(locale)); diff --git a/test/Discord.Net.Tests.Unit/CommandBuilderTests.cs b/test/Discord.Net.Tests.Unit/CommandBuilderTests.cs new file mode 100644 index 000000000..e122f9cdd --- /dev/null +++ b/test/Discord.Net.Tests.Unit/CommandBuilderTests.cs @@ -0,0 +1,37 @@ +using System; +using Discord; +using Xunit; + +namespace Discord; + +public class CommandBuilderTests +{ + [Fact] + public void BuildSimpleSlashCommand() + { + var command = new SlashCommandBuilder() + .WithName("command") + .WithDescription("description") + .AddOption( + "option1", + ApplicationCommandOptionType.String, + "option1 description", + isRequired: true, + choices: new [] + { + new ApplicationCommandOptionChoiceProperties() + { + Name = "choice1", Value = "1" + } + }) + .AddOptions(new SlashCommandOptionBuilder() + .WithName("option2") + .WithDescription("option2 description") + .WithType(ApplicationCommandOptionType.String) + .WithRequired(true) + .AddChannelType(ChannelType.Text) + .AddChoice("choice1", "1") + .AddChoice("choice2", "2")); + command.Build(); + } +} From 525dd6048a5cd9232d87aadd5d3d36bcebd7668c Mon Sep 17 00:00:00 2001 From: Nikolay <59023595+kolya112@users.noreply.github.com> Date: Sat, 10 Sep 2022 11:03:14 +0300 Subject: [PATCH 153/153] [Docs] Context menu of slash commands supported on mobile (#2459) --- docs/guides/int_basics/application-commands/intro.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/guides/int_basics/application-commands/intro.md b/docs/guides/int_basics/application-commands/intro.md index f55d0a2fc..a59aca8f2 100644 --- a/docs/guides/int_basics/application-commands/intro.md +++ b/docs/guides/int_basics/application-commands/intro.md @@ -18,9 +18,6 @@ The name and description help users find your command among many others, and the Message and User commands are only a name, to the user. So try to make the name descriptive. They're accessed by right clicking (or long press, on mobile) a user or a message, respectively. -> [!IMPORTANT] -> Context menu commands are currently not supported on mobile. - All three varieties of application commands have both Global and Guild variants. Your global commands are available in every guild that adds your application. You can also make commands for a specific guild; they're only available in that guild.