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 01/50] [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 02/50] 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 03/50] 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 04/50] 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 05/50] 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 06/50] 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 07/50] 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 08/50] 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 09/50] 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 10/50] 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 11/50] 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 12/50] 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 13/50] 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 14/50] 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 15/50] 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 16/50] 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 17/50] 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 18/50] 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 19/50] 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 20/50] 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 21/50] 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 22/50] 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 23/50] 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 24/50] 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 25/50] 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 26/50] 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 27/50] 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 28/50] 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 29/50] 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 30/50] 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 31/50] 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 32/50] 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 33/50] 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 34/50] 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 35/50] 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 36/50] 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 37/50] 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 38/50] 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 39/50] 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 40/50] 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 41/50] 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 42/50] 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 43/50] 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 44/50] 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 45/50] 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 zcmc$mbyQUS-u6|xySqVBq-*F-K^c)$l#=co9lAkEx*dB^)c z=Q+=bC*D}k`v`x7d)Qp-6QP!{dK?Bxy<_B zJ+pibWd$Q2^MkgX7j!?`V7CZ0M<*|V%Z&%@lx&beuP?a7)Wp&+Y36)lp100f{-U1a z`%X<9eV;{MI7d56dvo17Rr|x5q{gQ8o2>4fpWVZOL%!yq+;S&={!hhD316U}sB-a| zD;y4Xucu0`ZvmsOI9CAV7IL{SkqsX+7-~rm{Al@z8Xt2_{~LNV)KZ8#`~fHM_g^H) zg`h8msPVt&zr&$Ky-A(Y4rQzZMX@ zaoZq%!!I_&2xA-W#iB6_M01GOWs_F+?UWE8uTwRCwYqFy%!@DZ5Nj}v)5>J&+xLDp zYP~dO7F#OUzAM3wmlsi>TGi)*S!-jXvU}NgvC$l0e{0$A>aXl-7F1qU65IUfA(+uF zO;qVmT*GyB?V9TtlnDulciW8XW_f%MTA=uJZ6$5(gX_My-U$?WL*i#KXL;p^yAdpj zeG-$2r#6camI`u|uZ)L*^wqOWLap-q_T6)?J7%#ZPvRsLBgFtRq2a(dF03CEEaJE7POd%Z1)91`UrferT-ZXYT~&ZaL{vaWyrnSTam%;BrG`4w zCoW0YNp|>K!`WTwyB^R59-vr^==W1t5&FROc8Pm`)K)U3UCz-Dl|?8i-xxC|C)>l|0@dqVyV^jpcN57A zy<}4kAuE|I>}uRFt-0Qi3fKs|;d!!LW9X3VW+EOS>;tkk@3PBUX%1yYoxw-nQS=KK%|B7duIV$NDXTC!;j#YOAx*3`~hlpB9m2^J4p| z5z9`+zI#ff;1uxImT}m10IOGp)iVeujOzm1SYPfw_A{y3D#Aieexr{jCoI4ppVvO_ z|4>Yb?{-;Hp%#7)5i3#q+Wpv6pR_jE(8pzi-Kan{Wlcn)p*8Amt;!~8mecC-RCDp9 z@s%dRp?dB(@A2SHJC($t1m<+45Y67gcn{tb=jM-ruW_QR8I2k^Q&Yvl4aY?@deHJ6 z3f1J=`|8=^g|Dhc;P{Tq^s+UJ!wddCt+^oq>wt8%-U_a5me{7C7BrmERbhQLheJ7& z_9eyXhFYv=aq6SAv{Wgfh0%tsEi0L7W$n+GpMA1JWDhX(&9PA271QuDU9gBgUk7}; z7s`}A0v`jvc_A$l&)DG#y*B1%;z5U+gdgJUC;%iBsw4%WtE`XV=XJLWs(v$PWi-61 zA%P;_CoSHs)WNXJU`doJ1XO9i?%(cd*gjDienwQVHfO~i{YIlP!MbdVQNF`OPb%|t z#jjI^GH*YV-w1}Atyw&B`r}1M#83J$xn~6X&moFZRfT)LOso4Fn;Jj2*=mO;SdXS( zl-c5XEy5~u5pDBMB5jN?VlgZBS!*NdBOZk+L{kyxL`ROQQ}cjq*#&L$od+RK&XU3h zMo)mOvU!OIV-Gqr@Vwe(IL*?PWTR0E5PX6|_LI)R$K`3fcD!bn%JesNHB$Gs0408} zwFQkC2Ql8CB|N5fKNLc4Y4R0vl^SL@xGJZx=WgG0M|Pf2v}b{&7M?$T*>&DC-uS3R z^R(gJ$(<(q!OQ-{RSU>lY8vWqQJ(l*g@pB%2CGhjc{XNyt%Ol}%l@w)b$PSXtzSuV zdLVbnKb$=wHq!@#ZHf#S2Uizt;hghqhZgt>W+kdI^^CJy&$8&*Wi?v-SJz3K)vJMK z+J3g42P1(F3Mnl8G;)p!E$!Lz1~!B*@iYyNcPQ=h2CnQR?C4UG9WOVT6ixC{zkmSHYIoe{Y2Wg;Pp+Y(DUk1pQM$yl@p!Dv0SVCjXrU*z zF2225>Z65hejXtCr{fL8INWA|nZ(<)>yb?~*E}nEKXDZv@4ix+vTTcdWgAgFpMru+L6?jaS@rtAZYiFfq`+BIhqbA@Ca-7&@_l#U}Ndb7wbq9LZ}5-XA#jCGuoC^Hp@}H=?dsx zB*&v9K=Swn)#l$|NdpcVi{n88_n@n_TcdZU563hXHpX6z9uu(%s~DC$4S%>~yFC#z zIm9^#r}uDFX>(EyZ|a9HwBtcFeQQ@Y{UpwGVHbY~MZF{Mk_$B-P)sm8&;; zf#7t*ou3Axk?=L05`8{U>VoNwXsI@4TgWI6=^ZKdPUF8@2&vrhW-^Z9IAkp@*#_N^cnqdjdA68;mKMyYEDMNI_{hNHhg1{6Ny|S4&r8Q>5Jz{ZCu~+itHo*KMH{VJE*L=}NJ9nHY zq0U+25^}f$QzoaAsE^>RW~ks=?pi^DVX>M))PU?(49=$2Yo=N>4wMescx4+%=k8vD z#tZc5))1%gkbCIjAGcbeunFGjU29e=4e|4)-8aPUOiuOb=N%JzS3cm22ru^NAMg&= zE-hMIOkTb*l~Xu5!H#z6*y045vhJ`{E7@dTek2We;27?}Chro3`{f}_c+*44-|6R0 zZQyl3rwJEsLeuN6ANf9MFB%C&tEpU8KN1Td_t*FvJQ=aZ;Jk`GT1Pr8PY7C)L66>J zQ)a(8_*LZ@*UB(LVtJ{ir|-={X7K!z%x;fp?V{&Td0+3cB5ZXCBDrJE{Znk2e;s)B zp|P2#J6Cq*2j}|88NU`^(r+cvmdaeEY_i#TEg=_mT4g23(%v~bgz|PTKSUc=Xp;24 za@C7kWAkt~ROOY8#Ar&RF75eeEX*<$R~J+I-B>!GfC`4t^oQ%VJG4`**CKI@Ar3G6 z;06@Jr

{wXu3Jq%%xC5FQ*O!CC5$GEeWrlM=jQ+0jYM8$KnzEDn<_+Ely&~p^M-p+! zpWvY;#Z}dXVD)(K(WXz#_2u_xt3OLfG9xPAdFK*~QIsiX;?`y#X5t2j_vK{b>MFoW z#RY*!{%{wZx{)@&sGOx-w@hDRJj%AXR*6u7*>P{&RLD+2bV#+pGnNw5@YAJn9kxS& z^vbE<$oVi0`EE#}yhl`k;0tZ_Edmcv5Rs?gym$KOC7Ztu-hniO`k-+V?D0KWJCW%A zg$Bo)Aq$C4ca0`S_e5Btch}(@NJ#PVL1F(Q@3w~%U*yu{_joqt{aK__$n2dvBbKQP z%|i=Pg(3F^A{K_i9FRkU9=soKDsyJ)umZuLub`d%LM83<5G=2BpU)(yF;si>P{jfz zSjGS`b-z^eJ!9Uy)XUrZF7k};1*CH5Wo>DngFh#gK@7_{=2UNHtD3nh#&(-zblo8T zTRz>FjVxV($)qX4sai=u)1~gsSsAC?&&RVpTY>?SNK<983<15DKij>Z_i%uc!RnPb=EtQB6-K~ z0_^1%ThGtlV^7_f%b9BP^dnLF-DVJ>Czu73MUU2UP`KSI)rGDesk~va@ufWw)PM&_ z6D_m}EBg(4bh8Df%ADDU3o?BHakxBE777Z`ltS+`iOYQ9#&Y`EogH+3tDV2!rG1nb z6_3zh5#`wK2q;I33LP7gpWAx5tLDU&$2dPOVoMw~$B z)$p#n{#@_jop*OkXA-WRx9v&mtjTv?c-M21QK=@$IM3vywT_urrL=^YxE_%5k;UA4 zh+GN!r7qXm0lj#D_}}NixS&HaVNLiM`YG;G?$P7cvj-XGz!9GgY%5%>2d7{PaYvzGBsBcFTr<{gKWquwzR4bR!!)?Gl5@jQw>LPF%o`@bL zT0S*m73KP{uH&0yjq%5nASU=88Q&lC1Kj_^`2k0k8&tPh!lhBSIgfr}T!YFkrOhQ> z5Kbk#)2SQ$9jTCy8cHm%3Ifk4%a8I>{0lzKv?{7vXu0g$i!7RN+iJ zt*sXf+0iFJeT=!u_?R|TY=hHHB}1M^6NuqNcM%p;I9nc!+F6HlS0A5qe=sS8t$Z=& zHM1J&a>x|a>8x#kADf~klp~-3?p?{aOE|L#>e;10+Qxo0Zyf7#cC2r)t<8PH_nLKS z{4Y4a7~kd0gIFmq(WnjRz~}-E7H;a|bb1@KC(G|MwDDiMUGb}!%9#}fcz6#Ge;GLS zo$hO6^0W8-6YpJWj0P-Q@hXC-JW5VZ#^!Y^qrU?DY0(4H zPhrZRd`al6Dj%hGaB%>q)Wow5pUITgs-Fc=-ME?Dkg)3t9C7}#t5PA3N$52Ka%!oE+%zsos-=KMLQ{Sc27)ZTid<)2m^^p zFa8M7fxZW1bIy?0gwzY9G=*~Y++jb%jRRehPqqmp%}!Jqp5fE5%$`Ae>=PQqV(U9h ztd<8Y5=g>sCtg?GJ;1N#_{keEwkq--nmG*2Qk+|I+`f*BS^gfCeOFiwP@Xel)KEkJ z3h;~C{K9t%5EZjgu=@dQWfj}rVTgm9#|z!Q!5TFuhmbT)j&1Av;vRyN&qVvlsmM1FuN z4JJt(`BUc^CAV+WuR9JFo)u#ZPCZ+fF-Z||`C-Y;ftvV$4pNrF{{`_S>QLRKZ_`rm zsK2aAb1v&~YL&+EAAA$~OC*$+U$WIz_)cyqZy>V~_8)*=^>=UNG&JCsxE1RO z1+q#$aybw<>dlVO&a7&=9U6o5g1X>MUc zS9BBjeMg*4n>xQfOMOO+egtU2&Z^cogf?uhU~^GJDyThV(~}>bJJGYG!)B~@2+OUmpX_lXw-$49Y2r?S6l}jC5U7-&@Ho|-aB%~I% zjOeH+qsgCg#!c!+ccIA36sY8e^H1c3X&|l{^2Qfkr?)22(EeNYg%0&z2+_9B9Sq<)0vTxRm>WM}db7dAbwL#Sm6nCVfNBIbuA|gnHoBGF$ifiwUW|s zWL^Ej3;mi`FfB*1!7SQ&7=+IOY6^bag<+H(t9X6NPw%*+cB7gy!ps|ydwD9FyPvn> zCcN59Wd#&S3h)j$h9FXwS}6IBPOVVqF8P+M5-5B4OIsrQz)zsw6~CSa?TKs`E`id= zEkz$&0;gG1Fl!7%EQCkpZnn21@&&<=$}6rXD+5je?BBgamFLxL(}C5%wZg)h*;%;s(0Nh7uY)4ai2fC2p>8B3$DygJNn8|*q6vu9#8YD)NbARqx&p=EA%yg5TX0F8!JZmwDU z;UVPtt38yvy#syNYOzI&_qW>@k0~o#8AWd0F+-cRgV2eyNu7u+W<*9Ve)*O~Yg}ze zB_ng3a9wUInH@)CQn%Pfirlbw85PdXR3@^%-Wbz@G|6^Rz$J1K|U^i5}9S3b=0PJVZML+#KHqDH5lq?$3Ih zOg?8%LKX^>64R>gl?GgYuz5CmYbUt61}1T=uXm9@6Azro4H)~#9P!`Z|(yMqjP`W)^=~~t1`;`h> z5HX-OBc?khxHzS=kQO0L@~lhTy)1ENs@)j6nw!W22%5V=N&oB}cSi?rxwW-hY{Mwh zxy2vS+*-(I10v9>d z-$&dCLSG1w8uMEkB3;MTExhQR>#Jn~ZEUJen@8$Po(`k$4R-4Bmf(#?8Du;l4|>z} z4cuM~%M(K>1j`m?`)u%%@~?Rs*UevZ7zAlY(txflF}?tfo# zTY`H~)?SR#`$|jKM-Nw$QR2DheV6AL#>RGzZnJvDe&|5Jj~nl?P(T)!*^J#tV;l|2 z`@(px+Pplurs&sUr2B*>ftkt-q3=n?LeRXNljzw^W)h~tt6q(&X%t>NMcI05|&-B zJ_C}km$a@gyd93`&PW16>44=u7O|Zy4F^e5h_BKi^F+O|gC+>j@Se}zjTV0pATx3n zorN$iy8D2u?rKTTP2URO0=v?u32IJTj_HE7GX^<^Qf1E@B%3|>0# znAN0pIV+h|Lyr+k>=8sQI+JVb6?W@yTANy>2-Azj+KT3hq-t|Y2L%h)rZKzQgQb?| z7&BojZO$3ur?*$!9myEc+w!X~`iw?Ki!S@{@dVD)5i5NqJhgX&xLpmL6S@uygbkOy zLCLu`*F`fmmZRXGc1*n)@cWq8u^%{3;agzQ?OagA;@dPcW)A%`f`1VFQDm#cnOL9x zogL;ap1X`!dl3zhQRtej z(*vh!qm@#%9}!#|+}O_u?%=OE7%B0r+I66A8N*j^iFcpa=P1UP0~`x4m5#1%Pug)Q zsg#K(7r^y~$4UddcX2q`dnUQ1okyWv0>GmTZw{dI?#+pcOF)5(>MKvCj7w&qNFWX@ z&GPC(vaHNEBg(`Gv8cKGJR=RjLzr(_TzIf6WIv|JK_DTe?!?e*t(U6D+h*!R5~fXA z8M~9ncY;rqjDE08&S)x$lTv1IVfStnxR?6OVuklYFfGWUn-2E`@c0L|+=mFyao4wk zHp|>3)i+GWmNL^EK)no%8*}n1PNHhiX9dwnEn+_YpnCnNBh??z{QB52|a!IX_NEK;ki*uJMFgQ-$ zlNx`Wkg?yN*>Repq;cK&H+Rjqj_}qCi*z4T6uzZ#oprPje!p)w(a$KrDs08MqRgVq zb#KLeRI+a_Lv2e7FXQv)KHs8wnMl)HLvoP(#uoC%;7R*k|M2~^Nt`w!=;{Yt{VS3k z<om z>*MZWEL9|fwRak?T4-DcLZ5D}fKRiJCRleMlaYSE{!G2SY!9+DEK7Fj-l$Jj(M+2>2_YzC=$iSNZ89okj%5K5$ovG z=e84ec^fCJ2(45_eFZ_l_dEYeIL-G>RxP||7lphR<*l|nO*Xf!)BO3I!1g?km;U5AMZIFV^rz=kd7x$$gVK@c6g!X5u#ObVKHDjq45IX=WSL{ z(lsrJk=bKl+XAMJDSE+pU1Osqh4#F^3>7^?{70ARI%)wvN}h6U`igwY@HE%EMj>?1 zf1s?CE8~nH9tj=FyK_Fe9{4XGFBZ(eGx)ZaMwej6&)}RZZ|o@~LN|Q6V{)YfsUHp6 zr|$}X?uGP%`kCtFj;|X0n9CtOM?y6cB`#(v6i#G0lzMFNUK3Q^JYuWaHvddKkEfe| zQ6r!nW~AW2sQhX0T^XA-`PnM&SirfP>{>i^jz`e8slJ?M$AbLxwmmc#hz*oA*1pn} z5OYs5`7WG>!VhXvz07IDYJqxK1>-&&t{)P;c5aS21O(aUKDo4LcjMNu*p(q6W=(Lb zA-dDv&!u-ur_)G(T)1Y{z95_9A(2yX*jjiW|9yssjJLzRCr+A>pqUHV;-N&sgvWRC z1A;D4H|D27gaLS|xNwW6U8c_!I|J55f*EdW!k%TyW=xU?Pd9@7q{dpJzjMbwMk}biX(m6i=^9i+_SV-QT7fDQPy3d`f81B_E2J~9R zg4`{>1bXR9ky^`&Y_;4uZVXd;WAVtGdMyDNht69T9pe=Q!I80ZsTvKL{$M_uRw2~- zwdMAFVlVh?!4jL&(Lbp6X5;oKFHPhd0*l0#wD~AJA)pp-(t9B9iG7|zG`jf zRnx>9k=CGyR&7#q6~L^yscL!m_jHuY7pld+6L3M^)*&4VJn_KF1)M^sS`JmrGF>;0 z2yjwgpe27q^Yj(~;ISCDueucJe`vQf1DFKOsLnZm&&!q#%?cCI$b5T;K9JV)GHyWm zy;`u|*-Z%w6cO)JbCE|jNQ{A&JKrg)yC#S-`-NA&M{h_~8j)*3_ao-xOF>T5b)vO2 zM^zc}M%zy$Wd4ggP?YG5pnfe!u#wFwA}(6Btc(O@5?VGbZ|C>x?ZB0WK;L$@NFkT6 zSq{y)J2ff7Ti?IrGn-!S?cKR#gg9s>Tnf{k9z$U>&gEPE}K;xSt*9rg3knjlt$;XrS|kixT)_kUTAtJI5xg3uQ4cs zdE%+PxE^gc;4t#RI2ku@-b`W$>QY2pvEs|AqTR=#?5MgV*ZGbgf3wK69)y=l-Gq8( z*j+0cHRn*NP3FhXl$N z?-p{2DiP~ruuBZyXYu}GWV$vjInj>v!H(bG84W`eyNEZNh#5_SGsV*Ku-PgJtTf{~ zF2C5VJ_~kdfJPc!3Ez3A1mSESe!F2xTl@8d#EEHxc+&jc(#PnNVgE>!H!ND2S3n;C zyHUhvmpbJojdB8y-s3zY7H-H@1hFlwwa+K#LDCC`+S9=$07qA{&oKAw;enNo`=@_g?MzhK zdsTaFV?4Qw*_hP^Q;XKuCv?y)!Q4YNE%J`8@ zKR-8>@0?N}vF8ljXa6yE+%<0wX^aSd-tJcj4;yN!>EyAgD$qc6?Q!Vs~|S%d68NxSe$~vi7j`N`#3?HSx4;V!0N%b$M<((+#>- zX7=IAf}3&k?p_SGb>QQ2WNV#2^z=D$eT}FHvEI%Ug*?5m2paNXb0b&o?BdzJW2X%9p}&Pw&eqQ*K9L_Yeir&+xYD-JK_79$2Jh(Dp7$VmF?sJ` znjMLzc!&1;j%`rX&vg%~sRpZr5&1jT8`v@wrq7UVH$RG{7@*{(n zB;e)I7av?o4p;!s9c=3tH;XIkNKFc>1Fm%Nf{ZW9;l*V=#4+G&r{kQx&nu?g9Ot~f z6dJ!Vm)Cj%K6sdLp)s4%(L4L_r5s+bE!LWZ8tr-WS@o;4kp*6*qB4YCd@XG9^;0 z!HvWIyu2|A#amU*W|ZD7ZH~ z(Io7&y0UwiusO#-1T@p2^=^@BY{dp)(?T7Q5*(pZU zov>K1Jm6nV1CF+ygF8*Yu>rtE2cn@G^lF{1uWHbj`-1a{#33b#256tsG#>BS=1K~{ z>DMgKe4)6kN``;6*8E|J=U|!q57M>kC~J+Dp#x-tdcEdo{oQXX+QOehaib4F zRvC-|;7=9Mj#jZ#cSby>(Ka&?RBmBC zK2Lqqjk2EP)^0=oF_imP#J~Sv`f{i-163iZZj&XK8NGxlmzjRt;Z9L>ZNGU|VX5qK zD~QYNs>^pQG#fK^?;{N=?|Ym;#pfqt=tNuaOM?oFa7%+~C9;oj*|r1*FTw*sk_0Z9 z{HDx0>*gey%9Pk0q`d~M1UtmDgI~Uvg_3x~g&e*>Ijvl06g(~nDrKc&-{P$K zw&z_ww~t*6-!uR05vo8`?bt4YGk%RJ_KXqg(~OSOB9-+d9obf70cr>X_-405-WQu7 zCaHUuoO35u(iT|O+g+{>kg(Rct&Wa~Rai}WnGW$PF=5B&9%RsmVBDMcEzaYsLZN`x zaSxZ9ktgTSs0v+Il~ky-Ai$kX_w>XO9Flg-mz3G3w9 z^AB!-?Ge8mv5uHH3p(N(!rgC9`&Mmsi+!-QLc|kFz5+DYw!vui6^P6djbJspB-fMaZzMtsQpR$=JJ1%e%Hd&ZP-&tBeP%GA!Thgn90Qa z#9MpYboyF`B1n)PCBvs%d7*xWOYwr>HL}phlodB4wQ5jYU3FHgBDSL~y-}H`-QiS> z2VWUa#C)Q`DRK1`U1b`0DI&|;%uSy3NwUvBq?+^mUtzxU#FqzaO#)ETOsW<8)8p%D zTBD>!bm-Ws4PNn;Q*z8uUt0q&a1nF6%q(X!oph^Kh^j(iaaLsZ&Yt2Y*Sw$-DiqrH zM<_JJH$=}Qi&MFfXZ;4(A`-;0 z1H&-R4hLmCRoBw)t?{>~W^&N)<*>XMP{i-n#D3-@;m7{pY{7TbM9ZN=|ZaWZGI zkr!nI{(b&9`xh316YXc^dv(Rf284+X4We;{SIgkcsxI*{N+P0>f!9*_OJ)jTUCQ+j z`|qH*n2S<57p!&ip)2h2C-v#&(@BcA?pXm03*@rHhyWoD#^<)t)!ZkT#)^x;FW?(7& zh;mIg3)A*!K;6ehXG$yJQ-kXR%>G?7f{}X|kJy1k{;&chkjYK3V3AnTTwp;b18(RHk zc3fms7JSvvXdZ~}_#Do@EBu+9=QGIZ8RzjX>$lK&kC%#uhXg&b5^SZ%aUF2VLzrl)W4IpbZlIBJur3s(iZ8Ne|3lL7k@< zPv=huC_fIZ&s3YS73D=xD~G$tVQAq>BHx$fMr(9c2l?YW#`Ye6VL`uWh_-zDv->Q* z#^77HAoi4?IuQrVmiBF~v6Tkz=`dcZrK?0uHML-jYad7rOfjLNDh-%H&k>QBw(WG# zs6H5YN^SmYA;@p{0t&B60IN*qFdYQ>BBi%E2!xipqGWIuU`pz8v!S~!uSaGCm^tb& zE-bv*Oso*5_K#`J0m|l_IMx4V&HhAPA)Oo_Jlg%D7MHRWE7jFQ5s5@qi=KkJvoX9N zXLU$70dd-Y8`DkxGfZERM1nV_3m}&=vliwmQg+gf=g&kCT@l)&$&lbSoKz3^QLZJ2 z$g8nJmi4&ou5#ax&JYGz0F(ho$-`7cQ6=_W$uRnF*Y|0V<}j1n9hxp6?OmN9o-0g| z)*9uc#206r}5%%O>&+p_yoAz@fKlFv|WC>hxG~k+7e1R#$Zh+ zkhlAme1~DNb2K%m*4YS$pdEZ=Cunx|gRsMjB`AOdld16iJM;lK)W&BX<|K6a0@D3k z5u?oJg8tC!04iLnqx=gj-revg3-dgvG{q4~KWacB->9DjA<_ST!@BIlhs~ed-&7ODjT7V>epMV}L6X@@n>sv(@;vM=M;~2o zlwcL(OdgESYs^sk+2@!nCsF{9gQymW+#N%2w&6X#$pP)5{gl(aT%1BsT*b{x!)@mx z|M|2`QgSiFiC^M|Bj=_3P6Zc1RcF4xVG#M}P~6hQ+w3Ss_t*KMh)?X-OHLrTYwVe*r49 z$h|$Wx3WU={87PTz>Dt$GVPObo!S&`wH^cxB!RT~@2Q-%1OerWszInCQ~jgS0+9gZ zT}sIkmB&!vwkw5sKt?=Kp}Mb;{qgDmE#jo$)#!p5L_&a;jA{C`V}Z2F46IT;X%lra zPBrOl08z&%o^t~*g)}RNQaj4l4%-pUn#-}Ml=*xKy*?2Z?lfTgq^|{f2Y|Q%d4H{jtV=JXPXaL1jl-hoH`P z`ZrVP(W|qmZTxTPaw}ctF6kC$!kf3~w*6j*<%pR6O2S(NLwnc)y=V`ry{;T9UU~}% z<4F#59~8%i0RG6JM)l`js{31*Z7$InyHPq;8ygt4&s}NgU8d=c{&#WwbX;Xs2(h9E z;dVm_(YM#a5A_jmNwNX^dEu?WCGkV7lkJ1gKaM5N)N3zVS*5pHo><|BA{Pilgz zwy5+Q>%(~zm@f1Tevevz!q%z-Hu{HYFli*ZFPG%UVPdP7$wJ|4wEonHdau(U^F)IY zL$uS)g}vwwCPH3RX|#m0nMtG7BJdrx>%z0FyM;QUestd4Yz`k{*KCfD@non#KF;?$ z*cqRK6+cu}PUff%3q|77C04%*EgxM{9jaq1j`x+lpBN}g_pBYJPYZ}UsYX>vF%t6` zWUJ#)@3WlO=6}bhkfZ;D!Ng4;>J}Tf)KP=p1Rl3V^(cg2wVm};w~^+%t>@xQLBYAt zG^t8b3Ph}HkJAabu!^QNsFAe)-h=O|Dw(CmLbEC@6Z$-H7EI%(^>~&1&L#IGsPn(w zmHj6yM+Io34RA_LQQS;OUzH@ltp@WLQ!#lhI5axW=(E_7B!F0v`6B}BiBEv4;rjI9 zQOaiUh!c$N7e%9mpA|);8nRd}BI7p!D7$_}g9`uqisB!hyCU7s^?sJ0CT}4cKzc9Y zzw}#Qbo%57ZhwK*sUCHCRzGX*%nP3uv&aas3q+&ijklCB1BaL~=zjW%TpSY&lsNw0 zLkgAs%J{12OYN`2g3+M2`hS*MeGTuxZ32;9p@T|&Fz&5DS;Q2ItB3xMtJ^YBTuszL zk)}t*!?ds{{fTZKbDHMfGvYb(&TOJ-s)5@SgI=n2W=mRSXBRvokG7 zoapJBrDAl4y(j4}e4e!Oqs2{B>HK5j72^yC_5-`iDJZm}<6f5o&e?<9S_z%EE{W-_ z@X-2>xseM78fYwq^sV7ItD~VWI|E7I-8N4~@7F%$#7q`OaUJfeab3a6#RFY>vML^^ zj;)}w?z6tw;3zo;9Z;^mbQ|N)jsy z?q08MktV=)2+s>2-gpv^rSffml89rl3AI* zmC7KG+V#f#Z5orOUZag7j7mvA?Nc@;x*p*yfeYqUP%8m?(C_BsAO9bD2OwbpPHMRW zW<|qXUSzbqM>8cARV6smKYj~bxJE*D*i6|K+QX`n68TDWV)a;fh{>B;-s2x?gMZkn zpgx=_;47M;Y9OjGsCa@Ay-%C!8zItIa14hmzPJ3tG9CDq^E;@De)*T6>cxK=RNdW( z{xhg*mywCrueQ@b$6et~^V0(Y0CS)mQeyTm&rgyiA4@TA@_MB400by{J7yH>7El4_ zKU#-}7v951;f5?oUSG0({O_68OX$&Srz0Jw*qCEA1)ELuz{N;g(ESBr-=x) zoRGsC7Ivls#ft@D+qYB+A7;VVf-UeJL)tLYKdA?=q@^h;sJ5*!#warOF-q@g_tU15E?Txypzq`|w-!kl`uRVjsN2`TN4$G8Z4#e4>zYuL_&dMCR0A ztixvX-W60t&28SDtn%LYk-Rx%l{gvjuFjDSGc2tYW3PFNUxo)o0uoY+lSORsA*J)6 z$}LANSEvd%auM82rxGzhQIa&lIc9mF3E?Z-uG3SIRHxuW!>dm?WS(=m|J0^X{pgl5 zuKv6*uu#)hh@x9>(Ah!mmqOQub$7gAC&ZMqiPF~xW55FL+bh_!xD@Yw^?NP;{`i%Q zu(zC9Lte5}a}t>+$#7&_UZUF z;^^URkUlHWquKs14K{Y*VkYjkY6Roh@|bF_VO{=-C`qy{lqs83GyAbWUaAvzwp?iz z%Dp4mZ>7pAP^{D>CWwn>d-+Q77Xmw~Lb5w%UUf%_E#t@<=MZ;|h^aL`$0AGZ$JUFo zx2~dhFBfM@qTgqZ=p|rgf3Z3-Vn{bwyTndyeHf{}%Cg3F^U5o#(%; zy*S|hX~d$YH4UL0EhU*^?w%VPh%Zk_TnZU2cGRxrCRpkK74N`8#~cq@ElC~;&{76{ z^LIgy7D@1!yW9%aPbHe9~OXSR{e9txt`hI4f;Z; z(WhuwxA1n7u*1=0f9QAjD!s9-Ht~2Xzr{FA-2gsLVs=z9fbd__94a`tco^01O$Xu= z=?I9b8l>Ht&E)bY;frbv2yt9vzg>0Qnh1<+B(o9?cpOd}>3HBz!FNZKe!A9BEU29~ znJzb7@3r;#ek+sJiyKxc3&(EI@-4Dd=_R_OLjwUpCEX9sdp-|W~P z-%4xg10B95hFJjpUESTYID8ywQ)RJH8>0BM^x-|z$L`$r_1x!!qG4s%uMCr}UgEI31O=C1AHo$*mHM2bwB%4c<1jZivuLxV5-er5N5TTqKFIthR zR|%+HU7$+lknK_JCcB6?zil8?>@$RmRr+0F`r;N!fufpOs10AJP}8+=Uy@l%PGdr= zsh7bzovV=G?Oi&czX$Cv((h*hP}lr2Q9mX||GF^>Oxf0Y_!ztP9^DMk7kzDql--9u zXpabV*CZi(7)|>at3EcG+lYBqlc=NaPi2{q92ua86gwk6m0ew$?sC>ivV{lXg&{N4 z2D3>+jtT;J+V}Yx*e|_IT3jUfWrb45eoxCko3nErSjeTf@qF))@~7mB!&B3}k(l~P zBsAijERr}3psG|rB9zYGwCq3Nd7t6@&s&Xz&&a6v7>EkvW(YO6M!L4Oz|k%G;8f@i zq+!}>v>y^)eOx}UD(0~bNXel3Lh_yV72N*`_;sMixa8{|cg;NtWP1h`{O_WZUFZ#4 z>pn|=?lMC212qSRM%ujbaGdrFX?)j-B-iVpL(6UB*{#cTHta0wkuwNm~dw;Uog+!v-f`2UVE*dD7}6$*vodv>a6G~5Bxeo)w!4-gfK9QtZX@<i_ZLD!l9P~o;oRBhw>h$O%He1Z(|VD~f?2uXX6h@gbeG=bGB3>)GNrt~mxOZv zA_*xoV(@-!ourbGDHS`uFGjh+EzDq3MdU~y6l6w_Jim?h*#& zM|i{w*G@c>H+)-yjI%GxH^IN*)hBP1NmcR_tfCcJ>iU4P_bUg41V#pu?j@~ejaNYH zRRxaz5POvvK)SE+^Ln%!X+Z3R?o^u&^~Ae1`Y)^oIo1GF|~Fe_woNh7XF(VuF8OX>LEnRi>6Y$5MYCqxIM2sAFw>nGulzb zDxFR=?BnCeJ2B;%rugr>NtM?&b+8G_fazB`iTmn(9)+=C^UG|}a~b)67l^beofP;m zS=qu=SEd=Su<4kCMtfGSTAZaBJvEOU9Ot?g*USkis$m1%q(2O4i9ZbKXpxZk<{#qK zbGLr)m5Cc@|J6@ou)>=!Sf2T7KS^H^;;ImJl6a@>l&owGu*0(4bjYcWT*!7bGayBB4N2UuGwe>c^X zxXxN}G0(lls9pN7s}R<^8|QPT4($12rD_q1Pcd5gH-)8Yx?d?Qs9H;0K%cob)4ZB! zS{QmG@M#T6A14-k>79Bp5pcOUxc&JL=X4Loaqo|H3mn~R^c@p+IsL*i56kcOyO~n4 zCvoaLv*ex73=)h1xo~AuO6Eg~V+6cn8t3u)eOzF+X?ovv(%YOQL(W+4v0?vBTuI^l zewE6X!!o@A$I>>Ii#Z;04sTlh$KNvRoz$=*TCSWto$)wl9PuYh?z0o!ONBukHzcQi zLR>#TkZc}DZhQqfqGspg#6+SFwM76ov0Q{AB{H zx1-pFBC5xVSlDLl&&&JI_gL$thTPsY#u3BrEO&5tXJ$$_njRG^0VC(Ovo(@hDN?Q! z<704~_EU%rkP*M6j64FFBNO|2NgjvQ_L6)LT?~)|Ho@ z)F~v*nG1gwfC5-R-Ky6=3(>t6f)Aa6?8UhCtOvhB9X$Y;I_IS{`K9Up_e{S3kcHEA zh=C!hN3=4LMuoe=18KX5I4W!s=wRM`7)`Oqmv!ugviSVwAZkn3PDCh4Gp{d@3d(IS z90`xIeSUfVB)YpJ#f!ZdN+36G+8XEKFy3v#g$~Yf?F<_EKv*+0oY=AiZQ(F~m#HX$ zPQZMpEn4EGi*{XTyD8KX@=j*PH}g47bdsw!9$(@#zjf;=qA9n5q{W3GS;r`3H-Ruk zUE+i$VGqC3e*^sC^y_zengiQPivXqUFidGrHKfA&7RA0SxgJ3P>5Qe-3*rf`9+N3o zV)Na>J|1_#C3>EU`ZoA9A(#ZE;bH3TO$D7PLx9555mOE-k{TK$8WrIKqD|oL!d~n0 zLxa6#EOxq&r2NH%>9MYF1Mx*eelkCF;A5)GAoc3+c7!942Zx?|$o8=rGxA3hP+oK0 zcBi=RrZo8UkE7eK?+(sApvI8hPBK46HzRbpjAsW)xRS6K^Gxv;E-oC9?ZKAWyDdyf z{MBW3{P+vlTg#HQ-+j}YKEFJGJY0_aXho&}!j{PDx4Bi+BvVjq#BkOD8}3Zw>!=YS zz&MMW<(X0w41x@x@_L;%V1apJI3mT&GyxQ58B>v2mNrY;@7pk_)__zc{N~HJSzjrQu1xydZ7E_WU)12ckVq zEG+QxBl;4Vsab>MQ2~UmpVYKbO4{kUw2fk)+^OA>++a zRiX0{RiWAh!pAyIVNHrsOv;jl!|y`BHX_TJ6Dt-N()?3S?_jvz_;by(Ovum~_n35? z_rO-M<^5n$i%WwmP|J(;VPj8ka_VJsCBhMgc0+h=8%!yjpjJbIy`*I?c^D3I_8c{` znfYjC1+kR`9onMK``kVvjAA4>Q$>2NSWd*qmop>m+iKmY?A;gMMSflYU}!moLx%11 zv4lMy_xtt7L(P3!#f=bKq0Gfm9yx-Vpo3j+eLnjyIuz{3D-vpgmEk>WE`^=X?o?D# z2qy zUM!_g9KK;DP90ffv3FBSs5J|+wY+YyZyh6%p^MZcrPgWIvJr4vyA;Q=bT{qT13-#g z_*LTturi^62;ZjKP`%c(DRsa{@S z-&rA$zxu{dNky)ORw41Q`7-42MXdwg9&jNiyIo9Pqf*}`vO=)-H6ASMdG%^cHK2dz z0{BNgQ4@`!(fSbY`vSX#%>?78>UOccfe5H_lkehsDBPoNe+Vpw8KxExfK^@~yf!8*kY>^=$CD1$UV(9DCFhAHnol%4zkCJoH|$_QgH3 zgZT5u7b2X+448H*Je6Z%(w0 zD9FZ~_Kc|J$pJT%9i%y(gqXa@$NL=h^1N1Awi|7x_B2k1l@n0RD zR(BeloV{sLZ{C`KBx|gmRfY+x;`SV6Y-fnPxD{B0p=5i38wsDzMacUB^V)2haWX1z zQNf{RhGlz8d3UaJyY`6;?$=qlK+JbnlrvSjUNy?bv7cq~+2EHr@blFg(wkEi}gQ6JL`D;}^AltP)R`!?bm#Xgs10 zRVSJWdL3%ecwqeDe85^$_HwPt(!Va5#;S<#kX3Oma28}di8px)BJ{q>>bH^h6pPZBg0eNrW?N`+eZGsG&8%U6-a=pu+=i ziW1)BaTu13o30^_T3RXdHNaG$-jH(eMng@q^#b?5$x!y#);^yzc;8COS7^5(H52Td z%rD?v%G!>(E&Qp0`Ee6S+$yU_ioRN$Y1L_<7aj8347 zWT)X^4K+&RIo$8PQrTiRFa^gi-YUYY%zP)9?(1|-XOe|ubzep)Ro~my99vfGl8tBr z6}4=YDE&EfxU!;%d_+0dkM5{)5i?McGKWs02`XFCguUch{iuG+yjG{2>m!_DzimyV zz6pE2ofJ>H=rSV=;+w_YIIsZjn6M}}ny5o%=nEWjcRESQv4Mbe##TQ@Fj$7y zqpKX8Wv8H~Qg+OW9WU^7g{;`cGL~~o96S*7)$QYh$1i(yxP`hV*w?pob!6QP!8ew! zkFLpX*+=)1b2rfk)%pVoS&mmGfL9A2a2@5C8sU>4mmId~3z! z;Qo*@Mae!oYpn#b;60XVunT&q&Jx@m4K49>3rGl5c<9IZS%^dcw=!5#TY>CRX`BZd1K7f=n3XL0R*<;>XCEeUI?l-JKoXC(yTQV`~y3u5q zVsr``(rF;^8MDB9S!i8oX+oCih0Zu-@aW|kJ2v2_UtI?8jel~iSEDaN^9?auGyD)R zI4*(%A9=qJl9JCMJ=hDBMm4Im7PE2P0lbkrTl1w)SQU_Rg`bPdisf!&2pz)-2^JEK&2Nt)H@*1%u7I zVf@c5jUQ`l}HBFKP4Iey!Sk-#UnAKB5m{XG{z0rlOm+ zl9?J8azxF~tBdgjqthgG-ib=B;Jk`e@k!1XGm98ph1OS1S=5D@oW5E~N5$KNEH|xl zb|xpdRdyz;1)kBFXqo6WXrQv&1b(zEv%T)yHD*rC2}npwLAPWaTt18 zheI3bx{0W!Sfe|E=K8aV%U|pVrz3XzI&+Px{X1*B^D-m2+jlmYSzW04m0MRE>gUNg zrVY6K{DDbMn>wg{RDp+29P!ndcJT3Np&%Db`AF`(A-T6eu!Ukhl6<+HRo|}5`D~7d zB9(Be-{7oIgKpNQ2&^9cNqfhS?Q4LV{vEIdXY!ip@1DYM=BP7~?Z;!U@EjwYZztvX zSan;_ledCAf&ghxq!P5~d1d+)B7BBBb?)3zsVuD0l}`mF*_|-{_A!ljL_3T!SfY0a z3KS5a7wWDO4iLPd@z8c6BGqo@f|Z4Df%cJ@se+9|YAy_!(lbWaP$ywd{+e6T)>2mE zo$FDk>r92*-Hn7Q)gt>r>sDs7@cgJ5@OjMJsDs`Q!X3^0zxd<-an}K0G@k6`+>b8+ 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 46/50] 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 47/50] 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 48/50] 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 49/50] 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 50/50] 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;